diff --git a/src/bin/dhcp6/tests/flq_unittest.cc b/src/bin/dhcp6/tests/flq_unittest.cc new file mode 100644 index 0000000000..798634f235 --- /dev/null +++ b/src/bin/dhcp6/tests/flq_unittest.cc @@ -0,0 +1,436 @@ +// Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::dhcp::test; + +namespace { + +/// @brief JSON configuration +std::string FLQ_CONFIG = + "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"min-preferred-lifetime\": 100," + "\"preferred-lifetime\": 200," + "\"max-preferred-lifetime\": 300," + "\"min-valid-lifetime\": 400," + "\"valid-lifetime\": 500," + "\"max-valid-lifetime\": 600," + "\"rebind-timer\": 250," + "\"renew-timer\": 250," + "\"adaptive-lease-time-threshold\": .5," + "\"cache-threshold\": 0.," + "\"subnet6\": [ { " + " \"subnet\": \"3000::/32\", " + " \"id\": 1," + " \"interface\": \"eth0\"," + " \"pd-pools\": [ {" + " \"prefix\": \"2001:db8:1::\"," + " \"prefix-len\": 48," + " \"delegated-len\": 51" + " } ]," + " \"pd-allocator\": \"flq\"" + " } ]" + "}"; + +/// @brief Test fixture class for testing FLQ / adaptive-lease-time-threshold. +class FLQTest : public Dhcpv6SrvTest { +public: + /// @brief Constructor. + /// + /// Sets up fake interfaces. + FLQTest() : Dhcpv6SrvTest(), iface_mgr_test_config_(true) { + } + + /// @brief Creates a DHCPv6 lease for an address and MAC address. + /// + /// @param address Lease address. + /// @param duid_seed a seed from which the DUID is generated. + /// @return Created lease pointer. + Lease6Ptr + createLease6(const IOAddress& address, uint64_t duid_seed) const { + vector duid_vec(sizeof(duid_seed)); + for (unsigned i = 0; i < sizeof(duid_seed); ++i) { + duid_vec[i] = (duid_seed >> i) & 0xFF; + } + DuidPtr duid(new DUID(duid_vec)); + Lease6Ptr lease(new Lease6(Lease::TYPE_PD, address, duid, 1, + 800, 1200, 1, HWAddrPtr(), 51)); + return (lease); + } + + /// @brief Interface Manager's fake configuration control. + IfaceMgrTestConfig iface_mgr_test_config_; +}; + +// Test allocation with empty pool. +TEST_F(FLQTest, empty) { + Dhcp6Client client(srv_); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Perform 4-way exchange with the server requesting the last prefix. + client.requestPrefix(1, 51, IOAddress("2001:db8:1:e000::")); + ASSERT_NO_THROW(client.doSARR()); + + // Server should have assigned a prefix. + ASSERT_EQ(1, client.getLeaseNum()); + Lease6Ptr lease = checkLease(client.getLease(0)); + ASSERT_TRUE(lease); + + // Make sure that the client has got the requested prefix. + EXPECT_EQ("2001:db8:1:e000::", lease->addr_.toText()); + EXPECT_EQ(51, lease->prefixlen_); + + // Preferred lifetime should be the config preferred-lifetime (200). + EXPECT_EQ(200, lease->preferred_lft_); + + // Valid lifetime should be the config valid-lifetime (500). + EXPECT_EQ(500, lease->valid_lft_); +} + +// Test allocation with almost full pool. +TEST_F(FLQTest, last) { + Dhcp6Client client(srv_); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Create leases for the first prefixes. + auto& lease_mgr = LeaseMgrFactory::instance(); + auto lease0 = createLease6(IOAddress("2001:db8:1::"), 1); + EXPECT_TRUE(lease_mgr.addLease(lease0)); + auto lease1 = createLease6(IOAddress("2001:db8:1:2000::"), 2); + EXPECT_TRUE(lease_mgr.addLease(lease1)); + auto lease2 = createLease6(IOAddress("2001:db8:1:4000::"), 3); + EXPECT_TRUE(lease_mgr.addLease(lease2)); + auto lease3 = createLease6(IOAddress("2001:db8:1:6000::"), 4); + EXPECT_TRUE(lease_mgr.addLease(lease3)); + auto lease4 = createLease6(IOAddress("2001:db8:1:8000::"), 5); + EXPECT_TRUE(lease_mgr.addLease(lease4)); + auto lease5 = createLease6(IOAddress("2001:db8:1:a000::"), 6); + EXPECT_TRUE(lease_mgr.addLease(lease5)); + auto lease6 = createLease6(IOAddress("2001:db8:1:c000::"), 7); + EXPECT_TRUE(lease_mgr.addLease(lease6)); + + // Perform 4-way exchange with the server. + client.requestPrefix(); + ASSERT_NO_THROW(client.doSARR()); + + // Server should have assigned a prefix. + ASSERT_EQ(1, client.getLeaseNum()); + Lease6Ptr lease = checkLease(client.getLease(0)); + ASSERT_TRUE(lease); + + // Make sure that the client has got the requested prefix. + EXPECT_EQ("2001:db8:1:e000::", lease->addr_.toText()); + EXPECT_EQ(51, lease->prefixlen_); + + // Preferred lifetime should be the config min-preferred-lifetime (100). + EXPECT_EQ(100, lease->preferred_lft_); + + // Valid lifetime should be the config min-valid-lifetime (400). + EXPECT_EQ(400, lease->valid_lft_); +} + +// Test allocation with an expired lease. +TEST_F(FLQTest, expired) { + Dhcp6Client client(srv_); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Create leases for the first prefixes. + auto& lease_mgr = LeaseMgrFactory::instance(); + auto lease0 = createLease6(IOAddress("2001:db8:1::"), 1); + EXPECT_TRUE(lease_mgr.addLease(lease0)); + auto lease1 = createLease6(IOAddress("2001:db8:1:2000::"), 2); + EXPECT_TRUE(lease_mgr.addLease(lease1)); + auto lease2 = createLease6(IOAddress("2001:db8:1:4000::"), 3); + EXPECT_TRUE(lease_mgr.addLease(lease2)); + auto lease3 = createLease6(IOAddress("2001:db8:1:6000::"), 4); + EXPECT_TRUE(lease_mgr.addLease(lease3)); + auto lease4 = createLease6(IOAddress("2001:db8:1:8000::"), 5); + EXPECT_TRUE(lease_mgr.addLease(lease4)); + auto lease5 = createLease6(IOAddress("2001:db8:1:a000::"), 6); + EXPECT_TRUE(lease_mgr.addLease(lease5)); + auto lease6 = createLease6(IOAddress("2001:db8:1:c000::"), 7); + EXPECT_TRUE(lease_mgr.addLease(lease6)); + auto lease7 = createLease6(IOAddress("2001:db8:1:e000::"), 8); + // Expired lease. + lease7->cltt_ -= 10000; + lease7->current_cltt_ -= 10000; + ASSERT_TRUE(lease7->expired()); + EXPECT_TRUE(lease_mgr.addLease(lease7)); + + // Perform 4-way exchange with the server. + client.requestPrefix(); + ASSERT_NO_THROW(client.doSARR()); + + // Server should have assigned a prefix. + ASSERT_EQ(1, client.getLeaseNum()); + Lease6Ptr lease = checkLease(client.getLease(0)); + ASSERT_TRUE(lease); + + // Make sure that the client has got the requested prefix. + EXPECT_EQ("2001:db8:1:e000::", lease->addr_.toText()); + EXPECT_EQ(51, lease->prefixlen_); + + // Preferred lifetime should be the config min-preferred-lifetime (100). + EXPECT_EQ(100, lease->preferred_lft_); + + // Valid lifetime should be the config min-valid-lifetime (400). + EXPECT_EQ(400, lease->valid_lft_); +} + +// Test allocation with a reclaimed lease. +TEST_F(FLQTest, reclaimed) { + Dhcp6Client client(srv_); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Create leases for the first prefixes. + auto& lease_mgr = LeaseMgrFactory::instance(); + auto lease0 = createLease6(IOAddress("2001:db8:1::"), 1); + EXPECT_TRUE(lease_mgr.addLease(lease0)); + auto lease1 = createLease6(IOAddress("2001:db8:1:2000::"), 2); + EXPECT_TRUE(lease_mgr.addLease(lease1)); + auto lease2 = createLease6(IOAddress("2001:db8:1:4000::"), 3); + EXPECT_TRUE(lease_mgr.addLease(lease2)); + auto lease3 = createLease6(IOAddress("2001:db8:1:6000::"), 4); + EXPECT_TRUE(lease_mgr.addLease(lease3)); + auto lease4 = createLease6(IOAddress("2001:db8:1:8000::"), 5); + EXPECT_TRUE(lease_mgr.addLease(lease4)); + auto lease5 = createLease6(IOAddress("2001:db8:1:a000::"), 6); + EXPECT_TRUE(lease_mgr.addLease(lease5)); + auto lease6 = createLease6(IOAddress("2001:db8:1:c000::"), 7); + EXPECT_TRUE(lease_mgr.addLease(lease6)); + auto lease7 = createLease6(IOAddress("2001:db8:1:e000::"), 8); + // Reclaimed lease. + lease7->cltt_ -= 10000; + lease7->current_cltt_ -= 10000; + ASSERT_TRUE(lease7->expired()); + lease7->state_ = Lease::STATE_EXPIRED_RECLAIMED; + EXPECT_TRUE(lease_mgr.addLease(lease7)); + + // Perform 4-way exchange with the server. + client.requestPrefix(); + ASSERT_NO_THROW(client.doSARR()); + + // Server should have assigned a prefix. + ASSERT_EQ(1, client.getLeaseNum()); + Lease6Ptr lease = checkLease(client.getLease(0)); + ASSERT_TRUE(lease); + + // Make sure that the client has got the requested prefix. + EXPECT_EQ("2001:db8:1:e000::", lease->addr_.toText()); + EXPECT_EQ(51, lease->prefixlen_); + + // Preferred lifetime should be the config min-preferred-lifetime (100). + EXPECT_EQ(100, lease->preferred_lft_); + + // Valid lifetime should be the config min-valid-lifetime (400). + EXPECT_EQ(400, lease->valid_lft_); +} +#if 0 +// Test renewal with almost empty pool. +TEST_F(FLQTest, renew) { + Dhcp6Client client(srv_, Dhcp6Client::SELECTING); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Obtain a lease from the server using the 4-way exchange. + boost::shared_ptr hint(new IOAddress("10.0.0.14")); + ASSERT_NO_THROW(client.doDORA(hint)); + + // Make sure that the server responded. + Pkt6Ptr resp = client.getContext().response_; + ASSERT_TRUE(resp); + + // Make sure that the server has responded with DHCPACK. + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client has got the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the valid-lifetime parameter value (200). + OptionUint32Ptr opt = boost::dynamic_pointer_cast< + OptionUint32>(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_EQ(200, opt->getValue()); + + // Age the lease. + auto& lease_mgr = LeaseMgrFactory::instance(); + Lease6Ptr lease = lease_mgr.getLease6(*hint); + ASSERT_TRUE(lease); + lease->cltt_ -= 150; + lease->current_cltt_ -= 150; + EXPECT_NO_THROW(lease_mgr.updateLease6(lease)); + + // Let's transition the client to Renewing state. + client.setState(Dhcp6Client::RENEWING); + + // Set the unicast destination address to indicate that it is a renewal. + client.setDestAddress(IOAddress("10.0.0.1")); + ASSERT_NO_THROW(client.doRequest()); + + // Make sure that renewal was ACKed. + resp = client.getContext().response_; + ASSERT_TRUE(resp); + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client renewed the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the valid-lifetime parameter value (200). + opt = boost::dynamic_pointer_cast(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_EQ(200, opt->getValue()); +} + +// Test renewal with full pool. +TEST_F(FLQTest, renewFull) { + Dhcp6Client client(srv_, Dhcp6Client::SELECTING); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Obtain a lease from the server using the 4-way exchange. + boost::shared_ptr hint(new IOAddress("10.0.0.14")); + ASSERT_NO_THROW(client.doDORA(hint)); + + // Make sure that the server responded. + Pkt6Ptr resp = client.getContext().response_; + ASSERT_TRUE(resp); + + // Make sure that the server has responded with DHCPACK. + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client has got the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the valid-lifetime parameter value (200). + OptionUint32Ptr opt = boost::dynamic_pointer_cast< + OptionUint32>(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_EQ(200, opt->getValue()); + + // Age the lease. + auto& lease_mgr = LeaseMgrFactory::instance(); + Lease6Ptr lease = lease_mgr.getLease6(*hint); + ASSERT_TRUE(lease); + lease->cltt_ -= 150; + lease->current_cltt_ -= 150; + EXPECT_NO_THROW(lease_mgr.updateLease6(lease)); + + // Create leases for the first addresses. + auto lease1 = createLease6(IOAddress("10.0.0.11"), 1); + EXPECT_TRUE(lease_mgr.addLease(lease1)); + auto lease2 = createLease6(IOAddress("10.0.0.12"), 2); + EXPECT_TRUE(lease_mgr.addLease(lease2)); + auto lease3 = createLease6(IOAddress("10.0.0.13"), 3); + EXPECT_TRUE(lease_mgr.addLease(lease3)); + + // Let's transition the client to Renewing state. + client.setState(Dhcp6Client::RENEWING); + + // Set the unicast destination address to indicate that it is a renewal. + client.setDestAddress(IOAddress("10.0.0.1")); + ASSERT_NO_THROW(client.doRequest()); + + // Make sure that renewal was ACKed. + resp = client.getContext().response_; + ASSERT_TRUE(resp); + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client renewed the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the min-valid-lifetime parameter value (100). + opt = boost::dynamic_pointer_cast(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_EQ(100, opt->getValue()); +} + +// Test renewal with full pool but remaining lifetime greater than minimal. +TEST_F(FLQTest, renewRemaining) { + Dhcp6Client client(srv_, Dhcp6Client::SELECTING); + + // Configure DHCP server. + configure(FLQ_CONFIG, *client.getServer()); + + // Obtain a lease from the server using the 4-way exchange. + boost::shared_ptr hint(new IOAddress("10.0.0.14")); + ASSERT_NO_THROW(client.doDORA(hint)); + + // Make sure that the server responded. + Pkt6Ptr resp = client.getContext().response_; + ASSERT_TRUE(resp); + + // Make sure that the server has responded with DHCPACK. + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client has got the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the valid-lifetime parameter value (200). + OptionUint32Ptr opt = boost::dynamic_pointer_cast< + OptionUint32>(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_EQ(200, opt->getValue()); + + // Age the lease but only by 50 seconds. + auto& lease_mgr = LeaseMgrFactory::instance(); + Lease6Ptr lease = lease_mgr.getLease6(*hint); + ASSERT_TRUE(lease); + lease->cltt_ -= 50; + lease->current_cltt_ -= 50; + EXPECT_NO_THROW(lease_mgr.updateLease6(lease)); + + // Create leases for the first addresses. + auto lease1 = createLease6(IOAddress("10.0.0.11"), 1); + EXPECT_TRUE(lease_mgr.addLease(lease1)); + auto lease2 = createLease6(IOAddress("10.0.0.12"), 2); + EXPECT_TRUE(lease_mgr.addLease(lease2)); + auto lease3 = createLease6(IOAddress("10.0.0.13"), 3); + EXPECT_TRUE(lease_mgr.addLease(lease3)); + + // Let's transition the client to Renewing state. + client.setState(Dhcp6Client::RENEWING); + + // Set the unicast destination address to indicate that it is a renewal. + client.setDestAddress(IOAddress("10.0.0.1")); + ASSERT_NO_THROW(client.doRequest()); + + // Make sure that renewal was ACKed. + resp = client.getContext().response_; + ASSERT_TRUE(resp); + ASSERT_EQ(DHCPACK, static_cast(resp->getType())); + + // Make sure that the client renewed the requested address. + EXPECT_EQ(*hint, resp->getYiaddr()); + + // Valid lifetime should be the remaining lifetime so ~150 seconds. + opt = boost::dynamic_pointer_cast(resp->getOption(DHO_DHCP_LEASE_TIME)); + ASSERT_TRUE(opt); + EXPECT_NEAR(150, opt->getValue(), 2); +} +#endif + +} diff --git a/src/bin/dhcp6/tests/meson.build b/src/bin/dhcp6/tests/meson.build index 56cb1a9c8d..fb84a2fe63 100644 --- a/src/bin/dhcp6/tests/meson.build +++ b/src/bin/dhcp6/tests/meson.build @@ -127,6 +127,7 @@ kea_dhcp6_tests = executable( 'dhcp6_test_utils.cc', 'dhcp6_unittests.cc', 'dhcp6to4_ipc_unittest.cc', + 'flq_unittest.cc', 'fqdn_unittest.cc', 'get_config_unittest.cc', 'hooks_unittest.cc', diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc index abd56db5ce..1b322d0e9f 100644 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@ -1750,8 +1750,19 @@ AllocEngine::reuseExpiredLease(Lease6Ptr& expired, ClientContext6& ctx, isc_throw(BadValue, "Attempt to recycle registered address"); } + bool use_min = false; if (expired->type_ != Lease::TYPE_PD) { prefix_len = 128; // non-PD lease types must be always /128 + } else { + auto const& threshold = ctx.subnet_->getAdaptiveLeaseTimeThreshold(); + if (!threshold.unspecified() && (threshold < 1.0)) { + auto const& occupancy = ctx.subnet_->getAllocator(Lease::TYPE_PD)-> + getOccupancyRate(expired->addr_, prefix_len, + ctx.query_->getClasses()); + if (occupancy >= threshold) { + use_min = true; + } + } } if (!ctx.fake_allocation_) { @@ -1764,12 +1775,19 @@ AllocEngine::reuseExpiredLease(Lease6Ptr& expired, ClientContext6& ctx, // address, lease type and prefixlen (0) stay the same expired->iaid_ = ctx.currentIA().iaid_; expired->duid_ = ctx.duid_; + expired->hwaddr_ = ctx.hwaddr_; // Calculate life times. - getLifetimes6(ctx, expired->preferred_lft_, expired->valid_lft_); + expired->preferred_lft_ = 0; + expired->valid_lft_ = 0; + if (use_min) { + getMinLifetimes6(ctx, expired->preferred_lft_, expired->valid_lft_); + } else { + getLifetimes6(ctx, expired->preferred_lft_, expired->valid_lft_); + } expired->reuseable_valid_lft_ = 0; - expired->cltt_ = time(NULL); + expired->cltt_ = time(0); expired->subnet_id_ = ctx.subnet_->getID(); expired->hostname_ = ctx.hostname_; expired->fqdn_fwd_ = ctx.fwd_dns_update_; @@ -2035,13 +2053,27 @@ Lease6Ptr AllocEngine::createLease6(ClientContext6& ctx, uint8_t prefix_len, CalloutHandle::CalloutNextStep& callout_status) { - if (ctx.currentIA().type_ != Lease::TYPE_PD) { - prefix_len = 128; // non-PD lease types must be always /128 - } - uint32_t preferred = 0; uint32_t valid = 0; - getLifetimes6(ctx, preferred, valid); + bool use_min = false; + if (ctx.currentIA().type_ != Lease::TYPE_PD) { + prefix_len = 128; // non-PD lease types must be always /128 + } else { + auto const& threshold = ctx.subnet_->getAdaptiveLeaseTimeThreshold(); + if (!threshold.unspecified() && (threshold < 1.0)) { + auto const& occupancy = ctx.subnet_->getAllocator(Lease::TYPE_PD)-> + getOccupancyRate(addr, prefix_len, + ctx.query_->getClasses()); + if (occupancy >= threshold) { + use_min = true; + } + } + } + if (use_min) { + getMinLifetimes6(ctx, preferred, valid); + } else { + getLifetimes6(ctx, preferred, valid); + } Lease6Ptr lease(new Lease6(ctx.currentIA().type_, addr, ctx.duid_, ctx.currentIA().iaid_, preferred,