Commit cfbe13ad authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[master] Merge branch 'trac3565' (host reservations mac+dhcpv6)

Conflicts:
	src/bin/dhcp6/dhcp6_srv.cc
parents 72b477cb a104287e
......@@ -2061,6 +2061,76 @@ temporarily override a list of interface names and listen on all interfaces.
reservation. Such a feature will be added in the upcoming Kea
releases.</para>
</section>
<section id="reservation4-mode">
<title>Fine Tuning IPv4 Host Reservation</title>
<note>
<para><command>reservation-mode</command> configuration parameter in DHCPv4
server is accepted, but not used in the Kea 0.9.1 beta. Full implementation
will be available in the upcoming releases.</para>
</note>
<para>Host reservation capability introduces additional restrictions for the
allocation engine during lease selection and renewal. In particular, three
major checks are necessary. First, when selecting a new lease, it is not
sufficient for a candidate lease to be not used by another DHCP client. It
also must not be reserved for another client. Second, when renewing a lease,
additional check must be performed whether the address being renewed is not
reserved for another client. Finally, when a host renews an address, the server
has to check whether there's a reservation for this host, so the exisiting
(dynamically allocated) address should be revoked and the reserved one be
used instead.
</para>
<para>Some of those checks may be unnecessary in certain deployments. Not
performing them may improve performance. The Kea server provides the
<command>reservation-mode</command> configuration parameter to select the
types of reservations allowed for the particular subnet. Each reservation
type has different constraints for the checks to be performed by the
server when allocating or renewing a lease for the client.
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.
</simpara></listitem>
<listitem><simpara> <command>out-of-pool</command> - allows only out of
pool host reservations. With this setting in place, the server may assume
that all host reservations are for addresses that do not belong to the
dynamic pool. Therefore it can skip the reservation checks when dealing
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 0.9.1 does not sanity check the reservations against
<command>reservation-mode</command>. Misconfiguration may cause problems.
</simpara></listitem>
<listitem><simpara>
<command>disabled</command> - host reservation support is disabled. As there
are no reservations, the server will skip all checks. Any reservations defined
will be completely ignored. As the checks are skipped, the server may
operate faster in this mode.
</simpara></listitem>
</itemizedlist>
</para>
<para>
An example configuration that disables reservation looks like follows:
<screen>
"Dhcp4": {
"subnet4": [
"subnet": "192.0.2.0/24",
<userinput>"reservation-mode": "disabled"</userinput>,
...
]
}
</screen>
</para>
</section>
</section>
<!-- end of host reservations section -->
......
......@@ -1976,6 +1976,74 @@ should include options from the isc option space:
releases.</para>
</section>
<section id="reservation6-mode">
<title>Fine Tuning IPv6 Host Reservation</title>
<note>
<para><command>reservation-mode</command> in the DHCPv6 server is
implemented in Kea 0.9.1 beta, but has not been tested and is
considered experimental.</para>
</note>
<para>Host reservation capability introduces additional restrictions for the
allocation engine during lease selection and renewal. In particular, three
major checks are necessary. First, when selecting a new lease, it is not
sufficient for a candidate lease to be not used by another DHCP client. It
also must not be reserved for another client. Second, when renewing a lease,
additional check must be performed whether the address being renewed is not
reserved for another client. Finally, when a host renews an address or a
prefix, the server has to check whether there's a reservation for this host,
so the existing (dynamically allocated) address should be revoked and the
reserved one be used instead.</para>
<para>Some of those checks may be unnecessary in certain deployments. Not
performing them may improve performance. The Kea server provides the
<command>reservation-mode</command> configuration parameter to select the
types of reservations allowed for the particular subnet. Each reservation
type has different constraints for the checks to be performed by the
server when allocating or renewing a lease for the client.
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.
</simpara></listitem>
<listitem><simpara> <command>out-of-pool</command> - allows only out of
pool host reservations. With this setting in place, the server may assume
that all host reservations are for addresses that do not belong to the
dynamic pool. Therefore it can skip the reservation checks when dealing
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 0.9.1 does not sanity check the reservations against
<command>reservation-mode</command>. Misconfiguration may cause problems.
</simpara></listitem>
<listitem><simpara>
<command>disabled</command> - host reservation support is disabled. As there
are no reservations, the server will skip all checks. Any reservations defined
will be completely ignored. As the checks are skipped, the server may
operate faster in this mode.
</simpara></listitem>
</itemizedlist>
</para>
<para>
An example configuration that disables reservation looks like follows:
<screen>
"Dhcp6": {
"subnet6": [
"subnet": "2001:db8:1::/64",
<userinput>"reservation-mode": "disabled"</userinput>,
...
]
}
</screen>
</para>
</section>
<!-- @todo: add support for per IA reservation (that specifies IAID in
the ip-addresses and prefixes) -->
</section>
......
......@@ -381,7 +381,15 @@
"item_default": "0.0.0.0"
} ]
}
} ]
},
{
"item_name": "reservation-mode",
"item_type": "string",
"item_optional": true,
"item_default": "all",
"item_description": "Specifies allowed host reservation types. Disabling unused modes may improve performance. Allowed values: disabled, off, out-of-pool, all"
}
]
}
},
......
......@@ -183,7 +183,8 @@ protected:
} else if ((config_id.compare("subnet") == 0) ||
(config_id.compare("interface") == 0) ||
(config_id.compare("client-class") == 0) ||
(config_id.compare("next-server") == 0)) {
(config_id.compare("next-server") == 0) ||
(config_id.compare("reservation-mode") == 0)) {
parser = new StringParser(config_id, string_values_);
} else if (config_id.compare("pools") == 0) {
parser = new Pools4ListParser(config_id, pools_);
......
......@@ -3496,8 +3496,79 @@ TEST_F(Dhcp4ParserTest, reservationBogus) {
EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
checkResult(x, 1);
}
/// The goal of this test is to verify that Host Reservation modes can be
/// specified on a per-subnet basis.
TEST_F(Dhcp4ParserTest, hostReservationPerSubnet) {
/// - Configuration:
/// - only addresses (no prefixes)
/// - 4 subnets with:
/// - 192.0.2.0/24 (all reservations enabled)
/// - 192.0.3.0/24 (out-of-pool reservations)
/// - 192.0.4.0/24 (reservations disabled)
/// - 192.0.5.0/24 (reservations not specified)
const char* hr_config =
"{ "
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
" \"pools\": [ { \"pool\": \"192.0.2.0/24\" } ],"
" \"subnet\": \"192.0.2.0/24\", "
" \"reservation-mode\": \"all\""
" },"
" {"
" \"pools\": [ { \"pool\": \"192.0.3.0/24\" } ],"
" \"subnet\": \"192.0.3.0/24\", "
" \"reservation-mode\": \"out-of-pool\""
" },"
" {"
" \"pools\": [ { \"pool\": \"192.0.4.0/24\" } ],"
" \"subnet\": \"192.0.4.0/24\", "
" \"reservation-mode\": \"disabled\""
" },"
" {"
" \"pools\": [ { \"pool\": \"192.0.5.0/24\" } ],"
" \"subnet\": \"192.0.5.0/24\""
" } ],"
"\"valid-lifetime\": 4000 }";
ElementPtr json = Element::fromJSON(hr_config);
ConstElementPtr result;
EXPECT_NO_THROW(result = configureDhcp4Server(*srv_, json));
// returned value should be 0 (success)
checkResult(result, 0);
// Let's get all subnets and check that there are 4 of them.
ConstCfgSubnets4Ptr subnets = CfgMgr::instance().getStagingCfg()->getCfgSubnets4();
ASSERT_TRUE(subnets);
const Subnet4Collection* subnet_col = subnets->getAll();
ASSERT_EQ(4, subnet_col->size()); // We expect 4 subnets
// Let's check if the parsed subnets have correct HR modes.
// Subnet 1
Subnet4Ptr subnet;
subnet = subnets->selectSubnet(IOAddress("192.0.2.1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_ALL, subnet->getHostReservationMode());
// Subnet 2
subnet = subnets->selectSubnet(IOAddress("192.0.3.1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_OUT_OF_POOL, subnet->getHostReservationMode());
// Subnet 3
subnet = subnets->selectSubnet(IOAddress("192.0.4.1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_DISABLED, subnet->getHostReservationMode());
// Subnet 4
subnet = subnets->selectSubnet(IOAddress("192.0.5.1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_ALL, subnet->getHostReservationMode());
}
}
......@@ -433,6 +433,13 @@
}
} ]
}
},
{
"item_name": "reservation-mode",
"item_type": "string",
"item_optional": true,
"item_default": "all",
"item_description": "Specifies allowed host reservation types. Disabling unused modes may improve performance. Allowed values: disabled, off, out-of-pool, all"
} ]
}
},
......
......@@ -1580,6 +1580,15 @@ Dhcpv6Srv::extendIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
// we extend, cancel or otherwise deal with the leases.
bool hints_present = !ctx.hints_.empty();
/// @todo: This was clarfied in draft-ietf-dhc-dhcpv6-stateful-issues that
/// the server is allowed to assign new leases in both Renew and Rebind. For
/// now, we only support it in Renew, because it breaks a lot of Rebind
/// unit-tests. Ultimately, whether we allow it or not, should be exposed
/// as configurable policy. See ticket #3717.
if (query->getType() == DHCPV6_RENEW) {
ctx.allow_new_leases_in_renewals_ = true;
}
Lease6Collection leases = alloc_engine_->renewLeases6(ctx);
// Ok, now we have the leases extended. We have:
......@@ -1747,6 +1756,12 @@ Dhcpv6Srv::extendIA_PD(const Subnet6Ptr& subnet, const DuidPtr& duid,
// we extend, cancel or otherwise deal with the leases.
bool hints_present = !ctx.hints_.empty();
/// @todo: The draft-ietf-dhc-dhcpv6-stateful-issues added a new capability
/// of the server to to assign new PD leases in both Renew and Rebind.
/// There's allow_new_leases_in_renewals_ in the ClientContext6, but we
/// currently not use it in PD yet. This should be implemented as part
/// of the stateful-issues implementation effort. See ticket #3718.
// Call Allocation Engine and attempt to renew leases. Number of things
// may happen. Leases may be extended, revoked (if the lease is no longer
// valid or reserved for someone else), or new leases may be added.
......
......@@ -390,7 +390,8 @@ protected:
} else if ((config_id.compare("subnet") == 0) ||
(config_id.compare("interface") == 0) ||
(config_id.compare("client-class") == 0) ||
(config_id.compare("interface-id") == 0)) {
(config_id.compare("interface-id") == 0) ||
(config_id.compare("reservation-mode") == 0)) {
parser = new StringParser(config_id, string_values_);
} else if (config_id.compare("pools") == 0) {
parser = new Pools6ListParser(config_id, pools_);
......
......@@ -3703,4 +3703,80 @@ TEST_F(Dhcp6ParserTest, macSourcesBogus) {
checkResult(status, 1);
}
/// The goal of this test is to verify that Host Reservation modes can be
/// specified on a per-subnet basis.
TEST_F(Dhcp6ParserTest, hostReservationPerSubnet) {
/// - Configuration:
/// - only addresses (no prefixes)
/// - 4 subnets with:
/// - 2001:db8:1::/64 (all reservations enabled)
/// - 2001:db8:2::/64 (out-of-pool reservations)
/// - 2001:db8:3::/64 (reservations disabled)
/// - 2001:db8:3::/64 (reservations not specified)
const char* HR_CONFIG =
"{"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet6\": [ { "
" \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
" \"subnet\": \"2001:db8:1::/48\", "
" \"reservation-mode\": \"all\""
" },"
" {"
" \"pools\": [ { \"pool\": \"2001:db8:2::/64\" } ],"
" \"subnet\": \"2001:db8:2::/48\", "
" \"reservation-mode\": \"out-of-pool\""
" },"
" {"
" \"pools\": [ { \"pool\": \"2001:db8:3::/64\" } ],"
" \"subnet\": \"2001:db8:3::/48\", "
" \"reservation-mode\": \"disabled\""
" },"
" {"
" \"pools\": [ { \"pool\": \"2001:db8:4::/64\" } ],"
" \"subnet\": \"2001:db8:4::/48\" "
" } ],"
"\"valid-lifetime\": 4000 }";
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
Element::fromJSON(HR_CONFIG)));
// returned value should be 0 (success)
checkResult(status, 0);
CfgMgr::instance().commit();
// Let's get all subnets and check that there are 4 of them.
ConstCfgSubnets6Ptr subnets = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6();
ASSERT_TRUE(subnets);
const Subnet6Collection* subnet_col = subnets->getAll();
ASSERT_EQ(4, subnet_col->size()); // We expect 4 subnets
// Let's check if the parsed subnets have correct HR modes.
// Subnet 1
Subnet6Ptr subnet;
subnet = subnets->selectSubnet(IOAddress("2001:db8:1::1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_ALL, subnet->getHostReservationMode());
// Subnet 2
subnet = subnets->selectSubnet(IOAddress("2001:db8:2::1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_OUT_OF_POOL, subnet->getHostReservationMode());
// Subnet 3
subnet = subnets->selectSubnet(IOAddress("2001:db8:3::1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_DISABLED, subnet->getHostReservationMode());
// Subnet 4
subnet = subnets->selectSubnet(IOAddress("2001:db8:4::1"));
ASSERT_TRUE(subnet);
EXPECT_EQ(Subnet::HR_ALL, subnet->getHostReservationMode());
}
};
......@@ -905,20 +905,38 @@ TEST_F(Dhcpv6SrvTest, pdRenewBasic) {
}
// This test verifies that incoming (invalid) RENEW with an address
// can be handled properly.
//
// This test checks 3 scenarios:
// 1. there is no such lease at all
// 2. there is such a lease, but it is assigned to a different IAID
// 3. there is such a lease, but it belongs to a different client
// can be handled properly. This has changed with #3565. The server
// is now able to allocate a lease in Renew if it's available.
// Previous testRenewReject is now split into 3 tests.
//
// expected:
// - returned REPLY message has copy of client-id
// - returned REPLY message has server-id
// - returned REPLY message has IA_NA that includes STATUS-CODE
// - No lease in LeaseMgr
TEST_F(Dhcpv6SrvTest, RenewReject) {
testRenewReject(Lease::TYPE_NA, IOAddress("2001:db8:1:1::dead"));
// This test checks the first scenario: There is no lease at all.
// The server will try to assign it. Since it is not used by anyone else,
// the server will assign it. This is convenient for various types
// of recoveries, e.g. when the server lost its database.
TEST_F(Dhcpv6SrvTest, RenewUnknown) {
// False means that the lease should not be created before renewal attempt
testRenewBasic(Lease::TYPE_NA, "2001:db8:1:1::abc", "2001:db8:1:1::abc",
128, false);
}
// This test checks that a client that renews existing lease, but uses
// a wrong IAID, will be processed correctly. As there is no lease for
// this (duid, type, iaid) tuple, this is treated as a new IA, regardless
// if the client inserted an address that is used in a different IA.
// After #3565 was implemented, the server will attempt to assign a lease.
// The one that client requested is already used with different IAID, so
// it will just pick a different lease. This is the second out of three
// scenarios tests by old RenewReject test.
TEST_F(Dhcpv6SrvTest, RenewWrongIAID) {
testRenewWrongIAID(Lease::TYPE_NA, IOAddress("2001:db8:1:1::abc"));
}
// This test checks whether client A can renew an address that is currently
// leased by client B. The server should detect that the lease belong to
// someone else and assign a different lease. This is the third out of three
// scenarios tests by old RenewReject test.
TEST_F(Dhcpv6SrvTest, RenewSomeoneElesesLease) {
testRenewSomeoneElsesLease(Lease::TYPE_NA, IOAddress("2001:db8::1"));
}
// This test verifies that incoming (invalid) RENEW with a prefix
......
......@@ -200,7 +200,7 @@ Dhcpv6SrvTest::createIA(isc::dhcp::Lease::Type lease_type,
void
Dhcpv6SrvTest::testRenewBasic(Lease::Type type, const std::string& existing_addr,
const std::string& renew_addr,
const uint8_t prefix_len) {
const uint8_t prefix_len, bool insert_before_renew) {
NakedDhcpv6Srv srv(0);
const IOAddress existing(existing_addr);
......@@ -213,24 +213,27 @@ Dhcpv6SrvTest::testRenewBasic(Lease::Type type, const std::string& existing_addr
// Check that the address we are about to use is indeed in pool
ASSERT_TRUE(subnet_->inPool(type, existing));
// Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
// value on purpose. They should be updated during RENEW.
Lease6Ptr lease(new Lease6(type, existing, duid_, iaid, 501, 502, 503, 504,
subnet_->getID(), HWAddrPtr(), prefix_len));
lease->cltt_ = 1234;
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
Lease6Ptr l;
if (insert_before_renew) {
// Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
// value on purpose. They should be updated during RENEW.
Lease6Ptr lease(new Lease6(type, existing, duid_, iaid, 501, 502, 503, 504,
subnet_->getID(), HWAddrPtr(), prefix_len));
lease->cltt_ = 1234;
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// Check that the lease is really in the database
Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, existing);
ASSERT_TRUE(l);
// Check that the lease is really in the database
l = LeaseMgrFactory::instance().getLease6(type, existing);
ASSERT_TRUE(l);
// Check that T1, T2, preferred, valid and cltt really set and not using
// previous (500, 501, etc.) values
EXPECT_NE(l->t1_, subnet_->getT1());
EXPECT_NE(l->t2_, subnet_->getT2());
EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
EXPECT_NE(l->valid_lft_, subnet_->getValid());
EXPECT_NE(l->cltt_, time(NULL));
// Check that T1, T2, preferred, valid and cltt really set and not using
// previous (500, 501, etc.) values
EXPECT_NE(l->t1_, subnet_->getT1());
EXPECT_NE(l->t2_, subnet_->getT2());
EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
EXPECT_NE(l->valid_lft_, subnet_->getValid());
EXPECT_NE(l->cltt_, time(NULL));
}
Pkt6Ptr req = createMessage(DHCPV6_RENEW, type, IOAddress(renew_addr),
prefix_len, iaid);
......@@ -299,6 +302,114 @@ Dhcpv6SrvTest::testRenewBasic(Lease::Type type, const std::string& existing_addr
EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(renew_addr));
}
void
Dhcpv6SrvTest::testRenewWrongIAID(Lease::Type type, const IOAddress& addr) {
NakedDhcpv6Srv srv(0);
const uint32_t transid = 1234;
const uint32_t valid_iaid = 234;
const uint32_t bogus_iaid = 456;
uint8_t prefix_len = (type == Lease::TYPE_PD) ? 128 : pd_pool_->getLength();
// Quick sanity check that the address we're about to use is ok
ASSERT_TRUE(subnet_->inPool(type, addr));
// Check that the lease is NOT in the database
Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, addr);
ASSERT_FALSE(l);
// GenerateClientId() also sets duid_
OptionPtr clientid = generateClientId();
// Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
// value on purpose. They should be updated during RENEW.
Lease6Ptr lease(new Lease6(type, addr, duid_, valid_iaid,
501, 502, 503, 504, subnet_->getID(),
HWAddrPtr(), prefix_len));
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// Pass it to the server and hope for a REPLY
// Let's create a RENEW
Pkt6Ptr renew = createMessage(DHCPV6_RENEW, type, IOAddress(addr), prefix_len,
bogus_iaid);
renew->addOption(clientid);
renew->addOption(srv.getServerID());
// The duid and address matches, but the iaid is different. The server could
// respond with NoBinding. However, according to
// draft-ietf-dhc-dhcpv6-stateful-issues-10, the server can also assign a
// new address. And that's what we expect here.
Pkt6Ptr reply = srv.processRenew(renew);
checkResponse(reply, DHCPV6_REPLY, transid);
// Check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr>
addr_opt = checkIA_NA(reply, bogus_iaid, subnet_->getT1(), subnet_->getT2());
ASSERT_TRUE(addr_opt);
// Check that we've got the an address
checkIAAddr(addr_opt, addr_opt->getAddress(), Lease::TYPE_NA);
// Check that we got a different address than was in the database.
EXPECT_NE(addr_opt->getAddress().toText(), addr.toText());
// Check that the lease is really in the database
l = checkLease(duid_, reply->getOption(D6O_IA_NA), addr_opt);
ASSERT_TRUE(l);
}
void
Dhcpv6SrvTest::testRenewSomeoneElsesLease(Lease::Type type, const IOAddress& addr) {
NakedDhcpv6Srv srv(0);
const uint32_t valid_iaid = 234;
const uint32_t transid = 1234;
uint8_t prefix_len = (type == Lease::TYPE_PD) ? 128 : pd_pool_->getLength();
// GenerateClientId() also sets duid_
OptionPtr clientid = generateClientId();
// Let's create a lease.
Lease6Ptr lease(new Lease6(type, addr, duid_, valid_iaid,
501, 502, 503, 504, subnet_->getID(),
HWAddrPtr(), prefix_len));
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// CASE 3: Lease belongs to a client with different client-id
Pkt6Ptr renew = createMessage(DHCPV6_RENEW, type, IOAddress(addr), prefix_len,
valid_iaid);
renew->addOption(generateClientId(13)); // generate different DUID (length 13)
renew->addOption(srv.getServerID());
// The iaid and address matches, but the duid is different.
// The server should not renew it, but assign something else.
Pkt6Ptr reply = srv.processRenew(renew);
checkResponse(reply, DHCPV6_REPLY, transid);
OptionPtr tmp = reply->getOption(D6O_IA_NA);
ASSERT_TRUE(tmp);
// Check that IA_?? was returned and that there's proper status code
// Check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr>