From 7cb7013bfb5b99b74105764362a16e125eec5b53 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Fri, 17 Aug 2018 14:27:57 -0400 Subject: [PATCH 1/6] [#13,!6] allocation engine modifications to support v6 global host reservations src/lib/dhcpsrv/alloc_engine.cc New functions: AllocEngine::ClientContext6::globalHost() AllocEngine::ClientContext6::hasGlobalReservation() AllocEngine::findGlobalReservation() AllocEngine::allocateGlobalReservedLeases6() Modified functions: AllocEngine::ClientContext6::currentHost() - modified to take into account a global host AllocEngine::findReservation() - modified to use findGlobalReservation() AllocEngine::allocateLeases6() - modified to use allocateGlobalReservation() AllocEngine::removeNonmatchingReservedLeases6() - modified to retain global reservations AllocEngine::extendLease6() - modified to bypass range and client mismatch disqualification for global reservations src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc TEST_F(AllocEngine6Test, globalHostDynamicAddress) TEST_F(AllocEngine6Test, globalHostReservedAddress) TEST_F(AllocEngine6Test, globalHostReservedPrefix) src/lib/dhcpsrv/tests/alloc_engine_utils.cc testStatistics() - changed to test against SUBNET_ID_UNSUSED src/lib/dhcpsrv/tests/alloc_engine_utils.h testStatistics() - changed subnet_id default --- src/lib/dhcpsrv/alloc_engine.cc | 270 ++++++++++++++---- src/lib/dhcpsrv/alloc_engine.h | 44 +++ .../dhcpsrv/tests/alloc_engine6_unittest.cc | 193 ++++++++++++- src/lib/dhcpsrv/tests/alloc_engine_utils.cc | 2 +- src/lib/dhcpsrv/tests/alloc_engine_utils.h | 10 +- 5 files changed, 460 insertions(+), 59 deletions(-) diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc index 90b9058de4..39aaabf604 100644 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@ -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,126 @@ 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 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()); + + // 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(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(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(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 +1385,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 +1525,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(-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(-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 +1903,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 diff --git a/src/lib/dhcpsrv/alloc_engine.h b/src/lib/dhcpsrv/alloc_engine.h index 198e32c9cd..b806af2285 100644 --- a/src/lib/dhcpsrv/alloc_engine.h +++ b/src/lib/dhcpsrv/alloc_engine.h @@ -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 @@ -811,6 +852,9 @@ private: void allocateReservedLeases6(ClientContext6& ctx, Lease6Collection& existing_leases); + 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 diff --git a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc index 8394fd79be..da90675c33 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc +++ b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc @@ -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 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 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 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 diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.cc b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc index 8fa12fa838..67e389c00c 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine_utils.cc +++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc @@ -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) { diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.h b/src/lib/dhcpsrv/tests/alloc_engine_utils.h index 1d83d505cd..9a0e9b5b66 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine_utils.h +++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.h @@ -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); From 2a90f95de03546c9bd2409fdd65447e077caf934 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Fri, 17 Aug 2018 14:27:57 -0400 Subject: [PATCH 2/6] [#13,!6] allocation engine modifications to support v6 global host reservations src/lib/dhcpsrv/alloc_engine.cc New functions: AllocEngine::ClientContext6::globalHost() AllocEngine::ClientContext6::hasGlobalReservation() AllocEngine::findGlobalReservation() AllocEngine::allocateGlobalReservedLeases6() Modified functions: AllocEngine::ClientContext6::currentHost() - modified to take into account a global host AllocEngine::findReservation() - modified to use findGlobalReservation() AllocEngine::allocateLeases6() - modified to use allocateGlobalReservation() AllocEngine::removeNonmatchingReservedLeases6() - modified to retain global reservations AllocEngine::extendLease6() - modified to bypass range and client mismatch disqualification for global reservations src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc TEST_F(AllocEngine6Test, globalHostDynamicAddress) TEST_F(AllocEngine6Test, globalHostReservedAddress) TEST_F(AllocEngine6Test, globalHostReservedPrefix) src/lib/dhcpsrv/tests/alloc_engine_utils.cc testStatistics() - changed to test against SUBNET_ID_UNSUSED src/lib/dhcpsrv/tests/alloc_engine_utils.h testStatistics() - changed subnet_id default --- src/lib/dhcpsrv/alloc_engine.cc | 270 ++++++++++++++---- src/lib/dhcpsrv/alloc_engine.h | 44 +++ .../dhcpsrv/tests/alloc_engine6_unittest.cc | 193 ++++++++++++- src/lib/dhcpsrv/tests/alloc_engine_utils.cc | 2 +- src/lib/dhcpsrv/tests/alloc_engine_utils.h | 10 +- 5 files changed, 460 insertions(+), 59 deletions(-) diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc index 90b9058de4..39aaabf604 100644 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@ -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,126 @@ 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 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()); + + // 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(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(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(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 +1385,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 +1525,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(-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(-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 +1903,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 diff --git a/src/lib/dhcpsrv/alloc_engine.h b/src/lib/dhcpsrv/alloc_engine.h index 198e32c9cd..b806af2285 100644 --- a/src/lib/dhcpsrv/alloc_engine.h +++ b/src/lib/dhcpsrv/alloc_engine.h @@ -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 @@ -811,6 +852,9 @@ private: void allocateReservedLeases6(ClientContext6& ctx, Lease6Collection& existing_leases); + 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 diff --git a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc index 8394fd79be..da90675c33 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc +++ b/src/lib/dhcpsrv/tests/alloc_engine6_unittest.cc @@ -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 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 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 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 diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.cc b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc index 8fa12fa838..67e389c00c 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine_utils.cc +++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.cc @@ -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) { diff --git a/src/lib/dhcpsrv/tests/alloc_engine_utils.h b/src/lib/dhcpsrv/tests/alloc_engine_utils.h index 1d83d505cd..9a0e9b5b66 100644 --- a/src/lib/dhcpsrv/tests/alloc_engine_utils.h +++ b/src/lib/dhcpsrv/tests/alloc_engine_utils.h @@ -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); From ac87dd2585eb68e22e07d2132e1d7b2f45ca27f6 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Mon, 20 Aug 2018 14:46:07 -0400 Subject: [PATCH 3/6] [#13,!6] - Added unit tests to bin/dhcp6 to verify global HRs src/bin/dhcp6/tests/dhcp6_test_utils.cc Minor mods to emit configuration/parsing errors src/bin/dhcp6/tests/host_unittest.cc HostTest::sarrTest() - new function for testing a SARR that should result in a lease TEST_F(HostTest, globalReservationsNA) TEST_F(HostTest, globalReservationsPD) - new tests to verify global HRs --- src/bin/dhcp6/tests/dhcp6_test_utils.cc | 11 +- src/bin/dhcp6/tests/host_unittest.cc | 230 +++++++++++++++++++++++- 2 files changed, 238 insertions(+), 3 deletions(-) diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.cc b/src/bin/dhcp6/tests/dhcp6_test_utils.cc index b836b8fbc9..274c7a04c6 100644 --- a/src/bin/dhcp6/tests/dhcp6_test_utils.cc +++ b/src/bin/dhcp6/tests/dhcp6_test_utils.cc @@ -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(); } diff --git a/src/bin/dhcp6/tests/host_unittest.cc b/src/bin/dhcp6/tests/host_unittest.cc index 0028b3ddb0..0940dde2fc 100644 --- a/src/bin/dhcp6/tests/host_unittest.cc +++ b/src/bin/dhcp6/tests/host_unittest.cc @@ -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 NO 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 From 03c13b8d53f15b320adf67919a4585694ebd913e Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Tue, 21 Aug 2018 08:58:18 -0400 Subject: [PATCH 4/6] [#13,!6] - Updated v6 admin guide sections with global reservation info --- doc/guide/dhcp6-srv.xml | 157 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 7 deletions(-) diff --git a/doc/guide/dhcp6-srv.xml b/doc/guide/dhcp6-srv.xml index cf9cf3987c..3d44dec23a 100644 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@ -3103,7 +3103,7 @@ should include options from the isc option space: - + This feature is currently implemented for memfile backend. @@ -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. + + 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 + "global", Kea will look for host reservations only + among the global reservations defined. Typcially, such resrvations would + be used to reserve hostnames for clients which may move from one subnet + to another. + + + 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. + +
@@ -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. + + + 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. + +
@@ -3538,10 +3565,10 @@ should include options from the isc option space: Allowed values are: - all - 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. + all - 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. out-of-pool - 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 - reservation-mode and misconfiguration may cause problems. + reservation-mode and misconfiguration may cause + problems. + + + global - 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 global and + misconfiguration may cause problems. @@ -3576,9 +3614,44 @@ should include options from the isc option space: } ] } - + + + An example configuration using global reservations is shown below: + +"Dhcp6": { + + + "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" + } + ], + + "subnet6": [ + { + "subnet": "2001:db8:1::/64", + "reservation-mode": "global", + ... + }, + { + "subnet": "2001:db8:2::/64", + "reservation-mode": "global", + ... + } + ] +} + + For more details regarding global reservations, see + . + + 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: + +
+
+ Global reservations in DHCPv6 + + 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. + + 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::/64 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::/48, 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. + + + To use global host reservations a configuration similar to the following + can be used: + + +"Dhcp6:" { + // This specifies global reservations. They will apply to all subnets that + // have global reservations enabled. + + "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 2001:db8:1::/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" + } + ], + "valid-lifetime": 600, + "subnet4": [ { + "subnet": "2001:db8:1::/64", + "reservation-mode": "global", + "pools": [ { "pool": "2001:db8:1::-2001:db8:1::100" } ] + } ] +} + + + + When using database backends, the global host reservations are + distinguished from regular reservations by using subnet-id value of + zero. From 9253b4fa5648eabb238b96da8ab0123e4e66ac89 Mon Sep 17 00:00:00 2001 From: Tomek Mrugalski Date: Thu, 23 Aug 2018 14:41:02 +0200 Subject: [PATCH 5/6] [#13,!6] Updated AUTHORS file. --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 252bebe03e..6e328a189d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -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) From 91f8ca94f3f5b2232d31512e95b56560f26b8960 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Mon, 27 Aug 2018 13:23:17 -0400 Subject: [PATCH 6/6] [#13,!6] Addressed review comments Mostly corrections additions to commentary. --- doc/guide/dhcp6-srv.xml | 24 +++++++++++------------ src/bin/dhcp6/tests/host_unittest.cc | 4 ++-- src/lib/dhcpsrv/alloc_engine.cc | 3 ++- src/lib/dhcpsrv/alloc_engine.h | 29 ++++++++++++++++++++++------ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/doc/guide/dhcp6-srv.xml b/doc/guide/dhcp6-srv.xml index 3d44dec23a..ef57889451 100644 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@ -3275,7 +3275,7 @@ should include options from the isc option space: specific subnet. Kea will still match inbound client packets to a subnet as before, but when the subnet's reservation mode is set to "global", Kea will look for host reservations only - among the global reservations defined. Typcially, such resrvations would + among the global reservations defined. Typcially, such reservations would be used to reserve hostnames for clients which may move from one subnet to another. @@ -3709,17 +3709,17 @@ If not specified, the default value is: 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::/64 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::/48, which will be outside of the prefix announced + 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 + would be unsuable or at the very least ridden with issues, such as the downlink traffic not reaching the device. @@ -3741,7 +3741,7 @@ If not specified, the default value is: "hostname": "hw-host-fixed", // Use of IP address is global reservation is risky. If used outside of - // matching subnet, such as 2001:db8:1::/64, it will result in a broken + // matching subnet, such as 3001::/64, it will result in a broken // configuration being handled to the client. "ip-address": "2001:db8:ff::77" }, diff --git a/src/bin/dhcp6/tests/host_unittest.cc b/src/bin/dhcp6/tests/host_unittest.cc index 0940dde2fc..b3d9f33c3f 100644 --- a/src/bin/dhcp6/tests/host_unittest.cc +++ b/src/bin/dhcp6/tests/host_unittest.cc @@ -1180,8 +1180,8 @@ HostTest::sarrTest(Dhcp6Client& client, const std::string& exp_address, 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 NO hostname + // 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_); diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc index 39aaabf604..95f2e0d025 100644 --- a/src/lib/dhcpsrv/alloc_engine.cc +++ b/src/lib/dhcpsrv/alloc_engine.cc @@ -1252,7 +1252,7 @@ AllocEngine::allocateGlobalReservedLeases6(ClientContext6& ctx, return (false); } - // We want to avoid allocating new lease for an IA if there is already + // 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) { @@ -1266,6 +1266,7 @@ AllocEngine::allocateGlobalReservedLeases6(ClientContext6& ctx, .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. diff --git a/src/lib/dhcpsrv/alloc_engine.h b/src/lib/dhcpsrv/alloc_engine.h index b806af2285..e32e208f93 100644 --- a/src/lib/dhcpsrv/alloc_engine.h +++ b/src/lib/dhcpsrv/alloc_engine.h @@ -488,7 +488,7 @@ public: /// /// 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 + /// the global subnet), return it. Otherwise return an /// empty pointer. /// /// @return Pointer to the host object. @@ -783,7 +783,7 @@ public: 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_)); } @@ -842,16 +842,33 @@ 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);