From: Tomek Mrugalski Date: Tue, 23 Jul 2013 11:33:58 +0000 (+0200) Subject: [2984] DHCPv6 hooks tests moved to a separate file. X-Git-Tag: bind10-1.2.0beta1-release~284^2~16 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6f304d9ee4e85db2e3aa708ee31135c4e42ef347;p=thirdparty%2Fkea.git [2984] DHCPv6 hooks tests moved to a separate file. --- diff --git a/src/bin/dhcp6/tests/Makefile.am b/src/bin/dhcp6/tests/Makefile.am index e8f0fcae71..c55285db27 100644 --- a/src/bin/dhcp6/tests/Makefile.am +++ b/src/bin/dhcp6/tests/Makefile.am @@ -49,6 +49,8 @@ TESTS += dhcp6_unittests dhcp6_unittests_SOURCES = dhcp6_unittests.cc dhcp6_unittests_SOURCES += dhcp6_srv_unittest.cc +dhcp6_unittests_SOURCES += hook_unittest.cc +dhcp6_unittests_SOURCES += dhcp6_test_utils.h dhcp6_unittests_SOURCES += ctrl_dhcp6_srv_unittest.cc dhcp6_unittests_SOURCES += config_parser_unittest.cc dhcp6_unittests_SOURCES += ../dhcp6_srv.h ../dhcp6_srv.cc diff --git a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc index c5ced8db9d..6b8d12257b 100644 --- a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -26,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -35,20 +33,18 @@ #include #include #include -#include +#include #include #include #include #include #include #include -#include using namespace isc; +using namespace isc::test; using namespace isc::asiolink; -using namespace isc::config; -using namespace isc::data; using namespace isc::dhcp; using namespace isc::util; using namespace isc::hooks; @@ -58,353 +54,6 @@ using namespace std; // Maybe it should be isc::test? namespace { -class NakedDhcpv6Srv: public Dhcpv6Srv { - // "naked" Interface Manager, exposes internal members -public: - NakedDhcpv6Srv(uint16_t port) : Dhcpv6Srv(port) { - // Open the "memfile" database for leases - std::string memfile = "type=memfile"; - LeaseMgrFactory::create(memfile); - } - - /// @brief fakes packet reception - /// @param timeout ignored - /// - /// The method receives all packets queued in receive - /// queue, one after another. Once the queue is empty, - /// it initiates the shutdown procedure. - /// - /// See fake_received_ field for description - virtual Pkt6Ptr receivePacket(int /*timeout*/) { - - // If there is anything prepared as fake incoming - // traffic, use it - if (!fake_received_.empty()) { - Pkt6Ptr pkt = fake_received_.front(); - fake_received_.pop_front(); - return (pkt); - } - - // If not, just trigger shutdown and - // return immediately - shutdown(); - return (Pkt6Ptr()); - } - - /// @brief fake packet sending - /// - /// Pretend to send a packet, but instead just store - /// it in fake_send_ list where test can later inspect - /// server's response. - virtual void sendPacket(const Pkt6Ptr& pkt) { - fake_sent_.push_back(pkt); - } - - /// @brief adds a packet to fake receive queue - /// - /// See fake_received_ field for description - void fakeReceive(const Pkt6Ptr& pkt) { - fake_received_.push_back(pkt); - } - - virtual ~NakedDhcpv6Srv() { - // Close the lease database - LeaseMgrFactory::destroy(); - } - - using Dhcpv6Srv::processSolicit; - using Dhcpv6Srv::processRequest; - using Dhcpv6Srv::processRenew; - using Dhcpv6Srv::processRelease; - using Dhcpv6Srv::createStatusCode; - using Dhcpv6Srv::selectSubnet; - using Dhcpv6Srv::sanityCheck; - using Dhcpv6Srv::loadServerID; - using Dhcpv6Srv::writeServerID; - - /// @brief packets we pretend to receive - /// - /// Instead of setting up sockets on interfaces that change between OSes, it - /// is much easier to fake packet reception. This is a list of packets that - /// we pretend to have received. You can schedule new packets to be received - /// using fakeReceive() and NakedDhcpv6Srv::receivePacket() methods. - list fake_received_; - - list fake_sent_; -}; - -static const char* DUID_FILE = "server-id-test.txt"; - -// test fixture for any tests requiring blank/empty configuration -// serves as base class for additional tests -class NakedDhcpv6SrvTest : public ::testing::Test { -public: - - NakedDhcpv6SrvTest() : rcode_(-1) { - // it's ok if that fails. There should not be such a file anyway - unlink(DUID_FILE); - - const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces(); - - // There must be some interface detected - if (ifaces.empty()) { - // We can't use ASSERT in constructor - ADD_FAILURE() << "No interfaces detected."; - } - - valid_iface_ = ifaces.begin()->getName(); - } - - // Generate IA_NA option with specified parameters - boost::shared_ptr generateIA(uint32_t iaid, uint32_t t1, uint32_t t2) { - boost::shared_ptr ia = - boost::shared_ptr(new Option6IA(D6O_IA_NA, iaid)); - ia->setT1(t1); - ia->setT2(t2); - return (ia); - } - - /// @brief generates interface-id option, based on text - /// - /// @param iface_id textual representation of the interface-id content - /// - /// @return pointer to the option object - OptionPtr generateInterfaceId(const string& iface_id) { - OptionBuffer tmp(iface_id.begin(), iface_id.end()); - return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, tmp)); - } - - // Generate client-id option - OptionPtr generateClientId(size_t duid_size = 32) { - - OptionBuffer clnt_duid(duid_size); - for (int i = 0; i < duid_size; i++) { - clnt_duid[i] = 100 + i; - } - - duid_ = DuidPtr(new DUID(clnt_duid)); - - return (OptionPtr(new Option(Option::V6, D6O_CLIENTID, - clnt_duid.begin(), - clnt_duid.begin() + duid_size))); - } - - // Checks if server response (ADVERTISE or REPLY) includes proper server-id. - void checkServerId(const Pkt6Ptr& rsp, const OptionPtr& expected_srvid) { - // check that server included its server-id - OptionPtr tmp = rsp->getOption(D6O_SERVERID); - EXPECT_EQ(tmp->getType(), expected_srvid->getType() ); - ASSERT_EQ(tmp->len(), expected_srvid->len() ); - EXPECT_TRUE(tmp->getData() == expected_srvid->getData()); - } - - // Checks if server response (ADVERTISE or REPLY) includes proper client-id. - void checkClientId(const Pkt6Ptr& rsp, const OptionPtr& expected_clientid) { - // check that server included our own client-id - OptionPtr tmp = rsp->getOption(D6O_CLIENTID); - ASSERT_TRUE(tmp); - EXPECT_EQ(expected_clientid->getType(), tmp->getType()); - ASSERT_EQ(expected_clientid->len(), tmp->len()); - - // check that returned client-id is valid - EXPECT_TRUE(expected_clientid->getData() == tmp->getData()); - } - - // Checks if server response is a NAK - void checkNakResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type, - uint32_t expected_transid, - uint16_t expected_status_code) { - // Check if we get response at all - checkResponse(rsp, expected_message_type, expected_transid); - - // Check that IA_NA was returned - OptionPtr option_ia_na = rsp->getOption(D6O_IA_NA); - ASSERT_TRUE(option_ia_na); - - // check that the status is no address available - boost::shared_ptr ia = boost::dynamic_pointer_cast(option_ia_na); - ASSERT_TRUE(ia); - - checkIA_NAStatusCode(ia, expected_status_code); - } - - // Checks that server rejected IA_NA, i.e. that it has no addresses and - // that expected status code really appears there. In some limited cases - // (reply to RELEASE) it may be used to verify positive case, where - // IA_NA response is expected to not include address. - // - // Status code indicates type of error encountered (in theory it can also - // indicate success, but servers typically don't send success status - // as this is the default result and it saves bandwidth) - void checkIA_NAStatusCode(const boost::shared_ptr& ia, - uint16_t expected_status_code) { - // Make sure there is no address assigned. - EXPECT_FALSE(ia->getOption(D6O_IAADDR)); - - // T1, T2 should be zeroed - EXPECT_EQ(0, ia->getT1()); - EXPECT_EQ(0, ia->getT2()); - - OptionCustomPtr status = - boost::dynamic_pointer_cast(ia->getOption(D6O_STATUS_CODE)); - - // It is ok to not include status success as this is the default behavior - if (expected_status_code == STATUS_Success && !status) { - return; - } - - EXPECT_TRUE(status); - - if (status) { - // We don't have dedicated class for status code, so let's just interpret - // first 2 bytes as status. Remainder of the status code option content is - // just a text explanation what went wrong. - EXPECT_EQ(static_cast(expected_status_code), - status->readInteger(0)); - } - } - - void checkMsgStatusCode(const Pkt6Ptr& msg, uint16_t expected_status) { - OptionCustomPtr status = - boost::dynamic_pointer_cast(msg->getOption(D6O_STATUS_CODE)); - - // It is ok to not include status success as this is the default behavior - if (expected_status == STATUS_Success && !status) { - return; - } - - EXPECT_TRUE(status); - if (status) { - // We don't have dedicated class for status code, so let's just interpret - // first 2 bytes as status. Remainder of the status code option content is - // just a text explanation what went wrong. - EXPECT_EQ(static_cast(expected_status), - status->readInteger(0)); - } - } - - // Basic checks for generated response (message type and transaction-id). - void checkResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type, - uint32_t expected_transid) { - ASSERT_TRUE(rsp); - EXPECT_EQ(expected_message_type, rsp->getType()); - EXPECT_EQ(expected_transid, rsp->getTransid()); - } - - virtual ~NakedDhcpv6SrvTest() { - // Let's clean up if there is such a file. - unlink(DUID_FILE); - HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( - "pkt6_receive"); - HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( - "pkt6_send"); - HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( - "subnet6_select"); - - }; - - // A DUID used in most tests (typically as client-id) - DuidPtr duid_; - - int rcode_; - ConstElementPtr comment_; - - // Name of a valid network interface - string valid_iface_; -}; - -// Provides suport for tests against a preconfigured subnet6 -// extends upon NakedDhcp6SrvTest -class Dhcpv6SrvTest : public NakedDhcpv6SrvTest { -public: - /// Name of the server-id file (used in server-id tests) - - // these are empty for now, but let's keep them around - Dhcpv6SrvTest() { - subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 48, 1000, - 2000, 3000, 4000)); - pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:1::"), 64)); - subnet_->addPool(pool_); - - CfgMgr::instance().deleteSubnets6(); - CfgMgr::instance().addSubnet6(subnet_); - } - - // Checks that server response (ADVERTISE or REPLY) contains proper IA_NA option - // It returns IAADDR option for each chaining with checkIAAddr method. - boost::shared_ptr checkIA_NA(const Pkt6Ptr& rsp, uint32_t expected_iaid, - uint32_t expected_t1, uint32_t expected_t2) { - OptionPtr tmp = rsp->getOption(D6O_IA_NA); - // Can't use ASSERT_TRUE() in method that returns something - if (!tmp) { - ADD_FAILURE() << "IA_NA option not present in response"; - return (boost::shared_ptr()); - } - - boost::shared_ptr ia = boost::dynamic_pointer_cast(tmp); - if (!ia) { - ADD_FAILURE() << "IA_NA cannot convert option ptr to Option6"; - return (boost::shared_ptr()); - } - - EXPECT_EQ(expected_iaid, ia->getIAID()); - EXPECT_EQ(expected_t1, ia->getT1()); - EXPECT_EQ(expected_t2, ia->getT2()); - - tmp = ia->getOption(D6O_IAADDR); - boost::shared_ptr addr = boost::dynamic_pointer_cast(tmp); - return (addr); - } - - // Check that generated IAADDR option contains expected address - // and lifetime values match the configured subnet - void checkIAAddr(const boost::shared_ptr& addr, - const IOAddress& expected_addr, - uint32_t /* expected_preferred */, - uint32_t /* expected_valid */) { - - // Check that the assigned address is indeed from the configured pool. - // Note that when comparing addresses, we compare the textual - // representation. IOAddress does not support being streamed to - // an ostream, which means it can't be used in EXPECT_EQ. - EXPECT_TRUE(subnet_->inPool(addr->getAddress())); - EXPECT_EQ(expected_addr.toText(), addr->getAddress().toText()); - EXPECT_EQ(addr->getPreferred(), subnet_->getPreferred()); - EXPECT_EQ(addr->getValid(), subnet_->getValid()); - } - - // Checks if the lease sent to client is present in the database - // and is valid when checked agasint the configured subnet - Lease6Ptr checkLease(const DuidPtr& duid, const OptionPtr& ia_na, - boost::shared_ptr addr) { - boost::shared_ptr ia = boost::dynamic_pointer_cast(ia_na); - - Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(addr->getAddress()); - if (!lease) { - cout << "Lease for " << addr->getAddress().toText() - << " not found in the database backend."; - return (Lease6Ptr()); - } - - EXPECT_EQ(addr->getAddress().toText(), lease->addr_.toText()); - EXPECT_TRUE(*lease->duid_ == *duid); - EXPECT_EQ(ia->getIAID(), lease->iaid_); - EXPECT_EQ(subnet_->getID(), lease->subnet_id_); - - return (lease); - } - - ~Dhcpv6SrvTest() { - CfgMgr::instance().deleteSubnets6(); - }; - - // A subnet used in most tests - Subnet6Ptr subnet_; - - // A pool used in most tests - Pool6Ptr pool_; -}; - // This test verifies that incoming SOLICIT can be handled properly when // there are no subnets configured. // @@ -1832,689 +1481,6 @@ TEST_F(Dhcpv6SrvTest, ServerID) { EXPECT_EQ(duid1_text, text); } -// Checks if hooks are implemented properly. -TEST_F(Dhcpv6SrvTest, Hooks) { - NakedDhcpv6Srv srv(0); - - // check if appropriate hooks are registered - int hook_index_pkt6_received = -1; - int hook_index_select_subnet = -1; - int hook_index_pkt6_send = -1; - - // check if appropriate indexes are set - EXPECT_NO_THROW(hook_index_pkt6_received = ServerHooks::getServerHooks() - .getIndex("pkt6_receive")); - EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks() - .getIndex("subnet6_select")); - EXPECT_NO_THROW(hook_index_pkt6_send = ServerHooks::getServerHooks() - .getIndex("pkt6_send")); - - EXPECT_TRUE(hook_index_pkt6_received > 0); - EXPECT_TRUE(hook_index_select_subnet > 0); - EXPECT_TRUE(hook_index_pkt6_send > 0); -} - -// This function returns buffer for very simple Solicit -Pkt6* captureSimpleSolicit() { - Pkt6* pkt; - uint8_t data[] = { - 1, // type 1 = SOLICIT - 0xca, 0xfe, 0x01, // trans-id = 0xcafe01 - 0, 1, // option type 1 (client-id) - 0, 10, // option lenth 10 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // DUID - 0, 3, // option type 3 (IA_NA) - 0, 12, // option length 12 - 0, 0, 0, 1, // iaid = 1 - 0, 0, 0, 0, // T1 = 0 - 0, 0, 0, 0 // T2 = 0 - }; - - pkt = new Pkt6(data, sizeof(data)); - pkt->setRemotePort(546); - pkt->setRemoteAddr(IOAddress("fe80::1")); - pkt->setLocalPort(0); - pkt->setLocalAddr(IOAddress("ff02::1:2")); - pkt->setIndex(2); - pkt->setIface("eth0"); - - return (pkt); -} - -/// @brief a class dedicated to Hooks testing in DHCPv6 server -/// -/// This class has a number of static members, because each non-static -/// method has implicit 'this' parameter, so it does not match callout -/// signature and couldn't be registered. Furthermore, static methods -/// can't modify non-static members (for obvious reasons), so many -/// fields are declared static. It is still better to keep them as -/// one class rather than unrelated collection of global objects. -class HooksDhcpv6SrvTest : public Dhcpv6SrvTest { - -public: - - /// @brief creates Dhcpv6Srv and prepares buffers for callouts - HooksDhcpv6SrvTest() { - - // Allocate new DHCPv6 Server - srv_ = new NakedDhcpv6Srv(0); - - // clear static buffers - resetCalloutBuffers(); - } - - /// @brief destructor (deletes Dhcpv6Srv) - ~HooksDhcpv6SrvTest() { - delete srv_; - } - - /// @brief creates an option with specified option code - /// - /// This method is static, because it is used from callouts - /// that do not have a pointer to HooksDhcpv6SSrvTest object - /// - /// @param option_code code of option to be created - /// - /// @return pointer to create option object - static OptionPtr createOption(uint16_t option_code) { - - char payload[] = { - 0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14 - }; - - OptionBuffer tmp(payload, payload + sizeof(payload)); - return OptionPtr(new Option(Option::V6, option_code, tmp)); - } - - /// test callback that stores received callout name and pkt6 value - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_receive_callout(CalloutHandle& callout_handle) { - callback_name_ = string("pkt6_receive"); - - callout_handle.getArgument("query6", callback_pkt6_); - - callback_argument_names_ = callout_handle.getArgumentNames(); - return (0); - } - - /// test callback that changes client-id value - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_receive_change_clientid(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("query6", pkt); - - // get rid of the old client-id - pkt->delOption(D6O_CLIENTID); - - // add a new option - pkt->addOption(createOption(D6O_CLIENTID)); - - // carry on as usual - return pkt6_receive_callout(callout_handle); - } - - /// test callback that deletes client-id - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_receive_delete_clientid(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("query6", pkt); - - // get rid of the old client-id - pkt->delOption(D6O_CLIENTID); - - // carry on as usual - return pkt6_receive_callout(callout_handle); - } - - /// test callback that sets skip flag - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_receive_skip(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("query6", pkt); - - callout_handle.setSkip(true); - - // carry on as usual - return pkt6_receive_callout(callout_handle); - } - - /// Test callback that stores received callout name and pkt6 value - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_send_callout(CalloutHandle& callout_handle) { - callback_name_ = string("pkt6_send"); - - callout_handle.getArgument("response6", callback_pkt6_); - - callback_argument_names_ = callout_handle.getArgumentNames(); - return (0); - } - - // Test callback that changes server-id - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_send_change_serverid(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("response6", pkt); - - // get rid of the old server-id - pkt->delOption(D6O_SERVERID); - - // add a new option - pkt->addOption(createOption(D6O_SERVERID)); - - // carry on as usual - return pkt6_send_callout(callout_handle); - } - - /// test callback that deletes server-id - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_send_delete_serverid(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("response6", pkt); - - // get rid of the old client-id - pkt->delOption(D6O_SERVERID); - - // carry on as usual - return pkt6_send_callout(callout_handle); - } - - /// Test callback that sets skip flag - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - pkt6_send_skip(CalloutHandle& callout_handle) { - - Pkt6Ptr pkt; - callout_handle.getArgument("response6", pkt); - - callout_handle.setSkip(true); - - // carry on as usual - return pkt6_send_callout(callout_handle); - } - - /// Test callback that stores received callout name and subnet6 values - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - subnet6_select_callout(CalloutHandle& callout_handle) { - callback_name_ = string("subnet6_select"); - - callout_handle.getArgument("query6", callback_pkt6_); - callout_handle.getArgument("subnet6", callback_subnet6_); - callout_handle.getArgument("subnet6collection", callback_subnet6collection_); - - callback_argument_names_ = callout_handle.getArgumentNames(); - return (0); - } - - /// Test callback that picks the other subnet if possible. - /// @param callout_handle handle passed by the hooks framework - /// @return always 0 - static int - subnet6_select_different_subnet_callout(CalloutHandle& callout_handle) { - - // Call the basic calllout to record all passed values - subnet6_select_callout(callout_handle); - - const Subnet6Collection* subnets; - Subnet6Ptr subnet; - callout_handle.getArgument("subnet6", subnet); - callout_handle.getArgument("subnet6collection", subnets); - - // Let's change to a different subnet - if (subnets->size() > 1) { - subnet = (*subnets)[1]; // Let's pick the other subnet - callout_handle.setArgument("subnet6", subnet); - } - - return (0); - } - - /// resets buffers used to store data received by callouts - void resetCalloutBuffers() { - callback_name_ = string(""); - callback_pkt6_.reset(); - callback_subnet6_.reset(); - callback_subnet6collection_ = NULL; - callback_argument_names_.clear(); - } - - /// pointer to Dhcpv6Srv that is used in tests - NakedDhcpv6Srv* srv_; - - // The following fields are used in testing pkt6_receive_callout - - /// String name of the received callout - static string callback_name_; - - /// Pkt6 structure returned in the callout - static Pkt6Ptr callback_pkt6_; - - /// Pointer to a subnet received by callout - static Subnet6Ptr callback_subnet6_; - - /// A list of all available subnets (received by callout) - static const Subnet6Collection* callback_subnet6collection_; - - /// A list of all received arguments - static vector callback_argument_names_; -}; - -// The following fields are used in testing pkt6_receive_callout. -// See fields description in the class for details -string HooksDhcpv6SrvTest::callback_name_; -Pkt6Ptr HooksDhcpv6SrvTest::callback_pkt6_; -Subnet6Ptr HooksDhcpv6SrvTest::callback_subnet6_; -const Subnet6Collection* HooksDhcpv6SrvTest::callback_subnet6collection_; -vector HooksDhcpv6SrvTest::callback_argument_names_; - - -// Checks if callouts installed on pkt6_received are indeed called and the -// all necessary parameters are passed. -// -// Note that the test name does not follow test naming convention, -// but the proper hook name is "pkt6_receive". -TEST_F(HooksDhcpv6SrvTest, simple_pkt6_receive) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_receive", pkt6_receive_callout)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // check that the callback called is indeed the one we installed - EXPECT_EQ("pkt6_receive", callback_name_); - - // check that pkt6 argument passing was successful and returned proper value - EXPECT_TRUE(callback_pkt6_.get() == sol.get()); - - // Check that all expected parameters are there - vector expected_argument_names; - expected_argument_names.push_back(string("query6")); - - EXPECT_TRUE(expected_argument_names == callback_argument_names_); -} - -// Checks if callouts installed on pkt6_received is able to change -// the values and the parameters are indeed used by the server. -TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_receive) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_receive", pkt6_receive_change_clientid)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // check that the server did send a reposonce - ASSERT_EQ(1, srv_->fake_sent_.size()); - - // Make sure that we received a response - Pkt6Ptr adv = srv_->fake_sent_.front(); - ASSERT_TRUE(adv); - - // Get client-id... - OptionPtr clientid = adv->getOption(D6O_CLIENTID); - - // ... and check if it is the modified value - OptionPtr expected = createOption(D6O_CLIENTID); - EXPECT_TRUE(clientid->equal(expected)); -} - -// Checks if callouts installed on pkt6_received is able to delete -// existing options and that change impacts server processing (mandatory -// client-id option is deleted, so the packet is expected to be dropped) -TEST_F(HooksDhcpv6SrvTest, deleteClientId_pkt6_receive) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_receive", pkt6_receive_delete_clientid)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // Check that the server dropped the packet and did not send a response - ASSERT_EQ(0, srv_->fake_sent_.size()); -} - -// Checks if callouts installed on pkt6_received is able to set skip flag that -// will cause the server to not process the packet (drop), even though it is valid. -TEST_F(HooksDhcpv6SrvTest, skip_pkt6_receive) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_receive", pkt6_receive_skip)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // check that the server dropped the packet and did not produce any response - ASSERT_EQ(0, srv_->fake_sent_.size()); -} - - -// Checks if callouts installed on pkt6_send are indeed called and the -// all necessary parameters are passed. -TEST_F(HooksDhcpv6SrvTest, simple_pkt6_send) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_send", pkt6_send_callout)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // Check that the callback called is indeed the one we installed - EXPECT_EQ("pkt6_send", callback_name_); - - // Check that there is one packet sent - ASSERT_EQ(1, srv_->fake_sent_.size()); - Pkt6Ptr adv = srv_->fake_sent_.front(); - - // Check that pkt6 argument passing was successful and returned proper value - EXPECT_TRUE(callback_pkt6_.get() == adv.get()); - - // Check that all expected parameters are there - vector expected_argument_names; - expected_argument_names.push_back(string("response6")); - EXPECT_TRUE(expected_argument_names == callback_argument_names_); -} - -// Checks if callouts installed on pkt6_send is able to change -// the values and the packet sent contains those changes -TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_send) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_send", pkt6_send_change_serverid)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // check that the server did send a reposonce - ASSERT_EQ(1, srv_->fake_sent_.size()); - - // Make sure that we received a response - Pkt6Ptr adv = srv_->fake_sent_.front(); - ASSERT_TRUE(adv); - - // Get client-id... - OptionPtr clientid = adv->getOption(D6O_SERVERID); - - // ... and check if it is the modified value - OptionPtr expected = createOption(D6O_SERVERID); - EXPECT_TRUE(clientid->equal(expected)); -} - -// Checks if callouts installed on pkt6_send is able to delete -// existing options and that server applies those changes. In particular, -// we are trying to send a packet without server-id. The packet should -// be sent -TEST_F(HooksDhcpv6SrvTest, deleteServerId_pkt6_send) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_send", pkt6_send_delete_serverid)); - - // Let's create a simple SOLICIT - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // Check that the server indeed sent a malformed ADVERTISE - ASSERT_EQ(1, srv_->fake_sent_.size()); - - // Get that ADVERTISE - Pkt6Ptr adv = srv_->fake_sent_.front(); - ASSERT_TRUE(adv); - - // Make sure that it does not have server-id - EXPECT_FALSE(adv->getOption(D6O_SERVERID)); -} - -// Checks if callouts installed on pkt6_skip is able to set skip flag that -// will cause the server to not process the packet (drop), even though it is valid. -TEST_F(HooksDhcpv6SrvTest, skip_pkt6_send) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "pkt6_send", pkt6_send_skip)); - - // Let's create a simple REQUEST - Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); - - // Simulate that we have received that traffic - srv_->fakeReceive(sol); - - // Server will now process to run its normal loop, but instead of calling - // IfaceMgr::receive6(), it will read all packets from the list set by - // fakeReceive() - // In particular, it should call registered pkt6_receive callback. - srv_->run(); - - // check that the server dropped the packet and did not produce any response - ASSERT_EQ(0, srv_->fake_sent_.size()); -} - -// This test checks if subnet6_select callout is triggered and reports -// valid parameters -TEST_F(HooksDhcpv6SrvTest, subnet6_select) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "subnet6_select", subnet6_select_callout)); - - // Configure 2 subnets, both directly reachable over local interface - // (let's not complicate the matter with relays) - string config = "{ \"interfaces\": [ \"*\" ]," - "\"preferred-lifetime\": 3000," - "\"rebind-timer\": 2000, " - "\"renew-timer\": 1000, " - "\"subnet6\": [ { " - " \"pool\": [ \"2001:db8:1::/64\" ]," - " \"subnet\": \"2001:db8:1::/48\", " - " \"interface\": \"" + valid_iface_ + "\" " - " }, {" - " \"pool\": [ \"2001:db8:2::/64\" ]," - " \"subnet\": \"2001:db8:2::/48\" " - " } ]," - "\"valid-lifetime\": 4000 }"; - - ElementPtr json = Element::fromJSON(config); - ConstElementPtr status; - - // Configure the server and make sure the config is accepted - EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json)); - ASSERT_TRUE(status); - comment_ = parseAnswer(rcode_, status); - ASSERT_EQ(0, rcode_); - - // Prepare solicit packet. Server should select first subnet for it - Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); - sol->setRemoteAddr(IOAddress("fe80::abcd")); - sol->setIface(valid_iface_); - sol->addOption(generateIA(234, 1500, 3000)); - OptionPtr clientid = generateClientId(); - sol->addOption(clientid); - - // Pass it to the server and get an advertise - Pkt6Ptr adv = srv_->processSolicit(sol); - - // check if we get response at all - ASSERT_TRUE(adv); - - // Check that the callback called is indeed the one we installed - EXPECT_EQ("subnet6_select", callback_name_); - - // Check that pkt6 argument passing was successful and returned proper value - EXPECT_TRUE(callback_pkt6_.get() == sol.get()); - - const Subnet6Collection* exp_subnets = CfgMgr::instance().getSubnets6(); - - // The server is supposed to pick the first subnet, because of matching - // interface. Check that the value is reported properly. - ASSERT_TRUE(callback_subnet6_); - EXPECT_EQ(callback_subnet6_.get(), exp_subnets->front().get()); - - // Server is supposed to report two subnets - ASSERT_EQ(exp_subnets->size(), callback_subnet6collection_->size()); - - // Compare that the available subnets are reported as expected - EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet6collection_)[0].get()); - EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet6collection_)[1].get()); -} - -// This test checks if callout installed on subnet6_select hook point can pick -// a different subnet. -TEST_F(HooksDhcpv6SrvTest, subnet_select_change) { - - // Install pkt6_receive_callout - EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( - "subnet6_select", subnet6_select_different_subnet_callout)); - - // Configure 2 subnets, both directly reachable over local interface - // (let's not complicate the matter with relays) - string config = "{ \"interfaces\": [ \"*\" ]," - "\"preferred-lifetime\": 3000," - "\"rebind-timer\": 2000, " - "\"renew-timer\": 1000, " - "\"subnet6\": [ { " - " \"pool\": [ \"2001:db8:1::/64\" ]," - " \"subnet\": \"2001:db8:1::/48\", " - " \"interface\": \"" + valid_iface_ + "\" " - " }, {" - " \"pool\": [ \"2001:db8:2::/64\" ]," - " \"subnet\": \"2001:db8:2::/48\" " - " } ]," - "\"valid-lifetime\": 4000 }"; - - ElementPtr json = Element::fromJSON(config); - ConstElementPtr status; - - // Configure the server and make sure the config is accepted - EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json)); - ASSERT_TRUE(status); - comment_ = parseAnswer(rcode_, status); - ASSERT_EQ(0, rcode_); - - // Prepare solicit packet. Server should select first subnet for it - Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); - sol->setRemoteAddr(IOAddress("fe80::abcd")); - sol->setIface(valid_iface_); - sol->addOption(generateIA(234, 1500, 3000)); - OptionPtr clientid = generateClientId(); - sol->addOption(clientid); - - // Pass it to the server and get an advertise - Pkt6Ptr adv = srv_->processSolicit(sol); - - // check if we get response at all - ASSERT_TRUE(adv); - - // The response should have an address from second pool, so let's check it - OptionPtr tmp = adv->getOption(D6O_IA_NA); - ASSERT_TRUE(tmp); - boost::shared_ptr ia = boost::dynamic_pointer_cast(tmp); - ASSERT_TRUE(ia); - tmp = ia->getOption(D6O_IAADDR); - ASSERT_TRUE(tmp); - boost::shared_ptr addr_opt = - boost::dynamic_pointer_cast(tmp); - ASSERT_TRUE(addr_opt); - - // Get all subnets and use second subnet for verification - const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6(); - ASSERT_EQ(2, subnets->size()); - - // Advertised address must belong to the second pool (in subnet's range, - // in dynamic pool) - EXPECT_TRUE((*subnets)[1]->inRange(addr_opt->getAddress())); - EXPECT_TRUE((*subnets)[1]->inPool(addr_opt->getAddress())); -} - - /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test /// to call processX() methods. diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.h b/src/bin/dhcp6/tests/dhcp6_test_utils.h new file mode 100644 index 0000000000..d9bf29846a --- /dev/null +++ b/src/bin/dhcp6/tests/dhcp6_test_utils.h @@ -0,0 +1,392 @@ +// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +/// @file dhcp6_test_utils.h +/// +/// @brief This file contains utility classes used for DHCPv6 server testing + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace isc::dhcp; +using namespace isc::config; +using namespace isc::data; +using namespace isc::hooks; +using namespace isc::asiolink; +using namespace isc::util; +using namespace isc::hooks; + +namespace isc { +namespace test { + +/// @brief "naked" Dhcpv6Srv class that exposes internal members +class NakedDhcpv6Srv: public isc::dhcp::Dhcpv6Srv { +public: + NakedDhcpv6Srv(uint16_t port) : Dhcpv6Srv(port) { + // Open the "memfile" database for leases + std::string memfile = "type=memfile"; + LeaseMgrFactory::create(memfile); + } + + /// @brief fakes packet reception + /// @param timeout ignored + /// + /// The method receives all packets queued in receive + /// queue, one after another. Once the queue is empty, + /// it initiates the shutdown procedure. + /// + /// See fake_received_ field for description + virtual isc::dhcp::Pkt6Ptr receivePacket(int /*timeout*/) { + + // If there is anything prepared as fake incoming + // traffic, use it + if (!fake_received_.empty()) { + Pkt6Ptr pkt = fake_received_.front(); + fake_received_.pop_front(); + return (pkt); + } + + // If not, just trigger shutdown and + // return immediately + shutdown(); + return (Pkt6Ptr()); + } + + /// @brief fake packet sending + /// + /// Pretend to send a packet, but instead just store + /// it in fake_send_ list where test can later inspect + /// server's response. + virtual void sendPacket(const Pkt6Ptr& pkt) { + fake_sent_.push_back(pkt); + } + + /// @brief adds a packet to fake receive queue + /// + /// See fake_received_ field for description + void fakeReceive(const Pkt6Ptr& pkt) { + fake_received_.push_back(pkt); + } + + virtual ~NakedDhcpv6Srv() { + // Close the lease database + LeaseMgrFactory::destroy(); + } + + using Dhcpv6Srv::processSolicit; + using Dhcpv6Srv::processRequest; + using Dhcpv6Srv::processRenew; + using Dhcpv6Srv::processRelease; + using Dhcpv6Srv::createStatusCode; + using Dhcpv6Srv::selectSubnet; + using Dhcpv6Srv::sanityCheck; + using Dhcpv6Srv::loadServerID; + using Dhcpv6Srv::writeServerID; + + /// @brief packets we pretend to receive + /// + /// Instead of setting up sockets on interfaces that change between OSes, it + /// is much easier to fake packet reception. This is a list of packets that + /// we pretend to have received. You can schedule new packets to be received + /// using fakeReceive() and NakedDhcpv6Srv::receivePacket() methods. + std::list fake_received_; + + std::list fake_sent_; +}; + +static const char* DUID_FILE = "server-id-test.txt"; + +// test fixture for any tests requiring blank/empty configuration +// serves as base class for additional tests +class NakedDhcpv6SrvTest : public ::testing::Test { +public: + + NakedDhcpv6SrvTest() : rcode_(-1) { + // it's ok if that fails. There should not be such a file anyway + unlink(DUID_FILE); + + const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces(); + + // There must be some interface detected + if (ifaces.empty()) { + // We can't use ASSERT in constructor + ADD_FAILURE() << "No interfaces detected."; + } + + valid_iface_ = ifaces.begin()->getName(); + } + + // Generate IA_NA option with specified parameters + boost::shared_ptr generateIA(uint32_t iaid, uint32_t t1, uint32_t t2) { + boost::shared_ptr ia = + boost::shared_ptr(new Option6IA(D6O_IA_NA, iaid)); + ia->setT1(t1); + ia->setT2(t2); + return (ia); + } + + /// @brief generates interface-id option, based on text + /// + /// @param iface_id textual representation of the interface-id content + /// + /// @return pointer to the option object + OptionPtr generateInterfaceId(const std::string& iface_id) { + OptionBuffer tmp(iface_id.begin(), iface_id.end()); + return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, tmp)); + } + + // Generate client-id option + OptionPtr generateClientId(size_t duid_size = 32) { + + OptionBuffer clnt_duid(duid_size); + for (int i = 0; i < duid_size; i++) { + clnt_duid[i] = 100 + i; + } + + duid_ = DuidPtr(new DUID(clnt_duid)); + + return (OptionPtr(new Option(Option::V6, D6O_CLIENTID, + clnt_duid.begin(), + clnt_duid.begin() + duid_size))); + } + + // Checks if server response (ADVERTISE or REPLY) includes proper server-id. + void checkServerId(const Pkt6Ptr& rsp, const OptionPtr& expected_srvid) { + // check that server included its server-id + OptionPtr tmp = rsp->getOption(D6O_SERVERID); + EXPECT_EQ(tmp->getType(), expected_srvid->getType() ); + ASSERT_EQ(tmp->len(), expected_srvid->len() ); + EXPECT_TRUE(tmp->getData() == expected_srvid->getData()); + } + + // Checks if server response (ADVERTISE or REPLY) includes proper client-id. + void checkClientId(const Pkt6Ptr& rsp, const OptionPtr& expected_clientid) { + // check that server included our own client-id + OptionPtr tmp = rsp->getOption(D6O_CLIENTID); + ASSERT_TRUE(tmp); + EXPECT_EQ(expected_clientid->getType(), tmp->getType()); + ASSERT_EQ(expected_clientid->len(), tmp->len()); + + // check that returned client-id is valid + EXPECT_TRUE(expected_clientid->getData() == tmp->getData()); + } + + // Checks if server response is a NAK + void checkNakResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type, + uint32_t expected_transid, + uint16_t expected_status_code) { + // Check if we get response at all + checkResponse(rsp, expected_message_type, expected_transid); + + // Check that IA_NA was returned + OptionPtr option_ia_na = rsp->getOption(D6O_IA_NA); + ASSERT_TRUE(option_ia_na); + + // check that the status is no address available + boost::shared_ptr ia = boost::dynamic_pointer_cast(option_ia_na); + ASSERT_TRUE(ia); + + checkIA_NAStatusCode(ia, expected_status_code); + } + + // Checks that server rejected IA_NA, i.e. that it has no addresses and + // that expected status code really appears there. In some limited cases + // (reply to RELEASE) it may be used to verify positive case, where + // IA_NA response is expected to not include address. + // + // Status code indicates type of error encountered (in theory it can also + // indicate success, but servers typically don't send success status + // as this is the default result and it saves bandwidth) + void checkIA_NAStatusCode(const boost::shared_ptr& ia, + uint16_t expected_status_code) { + // Make sure there is no address assigned. + EXPECT_FALSE(ia->getOption(D6O_IAADDR)); + + // T1, T2 should be zeroed + EXPECT_EQ(0, ia->getT1()); + EXPECT_EQ(0, ia->getT2()); + + OptionCustomPtr status = + boost::dynamic_pointer_cast(ia->getOption(D6O_STATUS_CODE)); + + // It is ok to not include status success as this is the default behavior + if (expected_status_code == STATUS_Success && !status) { + return; + } + + EXPECT_TRUE(status); + + if (status) { + // We don't have dedicated class for status code, so let's just interpret + // first 2 bytes as status. Remainder of the status code option content is + // just a text explanation what went wrong. + EXPECT_EQ(static_cast(expected_status_code), + status->readInteger(0)); + } + } + + void checkMsgStatusCode(const Pkt6Ptr& msg, uint16_t expected_status) { + OptionCustomPtr status = + boost::dynamic_pointer_cast(msg->getOption(D6O_STATUS_CODE)); + + // It is ok to not include status success as this is the default behavior + if (expected_status == STATUS_Success && !status) { + return; + } + + EXPECT_TRUE(status); + if (status) { + // We don't have dedicated class for status code, so let's just interpret + // first 2 bytes as status. Remainder of the status code option content is + // just a text explanation what went wrong. + EXPECT_EQ(static_cast(expected_status), + status->readInteger(0)); + } + } + + // Basic checks for generated response (message type and transaction-id). + void checkResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type, + uint32_t expected_transid) { + ASSERT_TRUE(rsp); + EXPECT_EQ(expected_message_type, rsp->getType()); + EXPECT_EQ(expected_transid, rsp->getTransid()); + } + + virtual ~NakedDhcpv6SrvTest() { + // Let's clean up if there is such a file. + unlink(DUID_FILE); + HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( + "pkt6_receive"); + HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( + "pkt6_send"); + HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts( + "subnet6_select"); + + }; + + // A DUID used in most tests (typically as client-id) + DuidPtr duid_; + + int rcode_; + ConstElementPtr comment_; + + // Name of a valid network interface + std::string valid_iface_; +}; + +// Provides suport for tests against a preconfigured subnet6 +// extends upon NakedDhcp6SrvTest +class Dhcpv6SrvTest : public NakedDhcpv6SrvTest { +public: + /// Name of the server-id file (used in server-id tests) + + // these are empty for now, but let's keep them around + Dhcpv6SrvTest() { + subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 48, 1000, + 2000, 3000, 4000)); + pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:1::"), 64)); + subnet_->addPool(pool_); + + CfgMgr::instance().deleteSubnets6(); + CfgMgr::instance().addSubnet6(subnet_); + } + + // Checks that server response (ADVERTISE or REPLY) contains proper IA_NA option + // It returns IAADDR option for each chaining with checkIAAddr method. + boost::shared_ptr checkIA_NA(const Pkt6Ptr& rsp, uint32_t expected_iaid, + uint32_t expected_t1, uint32_t expected_t2) { + OptionPtr tmp = rsp->getOption(D6O_IA_NA); + // Can't use ASSERT_TRUE() in method that returns something + if (!tmp) { + ADD_FAILURE() << "IA_NA option not present in response"; + return (boost::shared_ptr()); + } + + boost::shared_ptr ia = boost::dynamic_pointer_cast(tmp); + if (!ia) { + ADD_FAILURE() << "IA_NA cannot convert option ptr to Option6"; + return (boost::shared_ptr()); + } + + EXPECT_EQ(expected_iaid, ia->getIAID()); + EXPECT_EQ(expected_t1, ia->getT1()); + EXPECT_EQ(expected_t2, ia->getT2()); + + tmp = ia->getOption(D6O_IAADDR); + boost::shared_ptr addr = boost::dynamic_pointer_cast(tmp); + return (addr); + } + + // Check that generated IAADDR option contains expected address + // and lifetime values match the configured subnet + void checkIAAddr(const boost::shared_ptr& addr, + const IOAddress& expected_addr, + uint32_t /* expected_preferred */, + uint32_t /* expected_valid */) { + + // Check that the assigned address is indeed from the configured pool. + // Note that when comparing addresses, we compare the textual + // representation. IOAddress does not support being streamed to + // an ostream, which means it can't be used in EXPECT_EQ. + EXPECT_TRUE(subnet_->inPool(addr->getAddress())); + EXPECT_EQ(expected_addr.toText(), addr->getAddress().toText()); + EXPECT_EQ(addr->getPreferred(), subnet_->getPreferred()); + EXPECT_EQ(addr->getValid(), subnet_->getValid()); + } + + // Checks if the lease sent to client is present in the database + // and is valid when checked agasint the configured subnet + Lease6Ptr checkLease(const DuidPtr& duid, const OptionPtr& ia_na, + boost::shared_ptr addr) { + boost::shared_ptr ia = boost::dynamic_pointer_cast(ia_na); + + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(addr->getAddress()); + if (!lease) { + std::cout << "Lease for " << addr->getAddress().toText() + << " not found in the database backend."; + return (Lease6Ptr()); + } + + EXPECT_EQ(addr->getAddress().toText(), lease->addr_.toText()); + EXPECT_TRUE(*lease->duid_ == *duid); + EXPECT_EQ(ia->getIAID(), lease->iaid_); + EXPECT_EQ(subnet_->getID(), lease->subnet_id_); + + return (lease); + } + + ~Dhcpv6SrvTest() { + CfgMgr::instance().deleteSubnets6(); + }; + + // A subnet used in most tests + Subnet6Ptr subnet_; + + // A pool used in most tests + Pool6Ptr pool_; +}; + +}; // end of isc::test namespace +}; // end of isc namespace diff --git a/src/bin/dhcp6/tests/hook_unittest.cc b/src/bin/dhcp6/tests/hook_unittest.cc new file mode 100644 index 0000000000..278f5a406b --- /dev/null +++ b/src/bin/dhcp6/tests/hook_unittest.cc @@ -0,0 +1,736 @@ +// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC") +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace isc; +using namespace isc::test; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::util; +using namespace isc::hooks; +using namespace std; + +// namespace has to be named, because friends are defined in Dhcpv6Srv class +// Maybe it should be isc::test? +namespace { + +// Checks if hooks are implemented properly. +TEST_F(Dhcpv6SrvTest, Hooks) { + NakedDhcpv6Srv srv(0); + + // check if appropriate hooks are registered + int hook_index_pkt6_received = -1; + int hook_index_select_subnet = -1; + int hook_index_pkt6_send = -1; + + // check if appropriate indexes are set + EXPECT_NO_THROW(hook_index_pkt6_received = ServerHooks::getServerHooks() + .getIndex("pkt6_receive")); + EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks() + .getIndex("subnet6_select")); + EXPECT_NO_THROW(hook_index_pkt6_send = ServerHooks::getServerHooks() + .getIndex("pkt6_send")); + + EXPECT_TRUE(hook_index_pkt6_received > 0); + EXPECT_TRUE(hook_index_select_subnet > 0); + EXPECT_TRUE(hook_index_pkt6_send > 0); +} + +// This function returns buffer for very simple Solicit +Pkt6* captureSimpleSolicit() { + Pkt6* pkt; + uint8_t data[] = { + 1, // type 1 = SOLICIT + 0xca, 0xfe, 0x01, // trans-id = 0xcafe01 + 0, 1, // option type 1 (client-id) + 0, 10, // option lenth 10 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // DUID + 0, 3, // option type 3 (IA_NA) + 0, 12, // option length 12 + 0, 0, 0, 1, // iaid = 1 + 0, 0, 0, 0, // T1 = 0 + 0, 0, 0, 0 // T2 = 0 + }; + + pkt = new Pkt6(data, sizeof(data)); + pkt->setRemotePort(546); + pkt->setRemoteAddr(IOAddress("fe80::1")); + pkt->setLocalPort(0); + pkt->setLocalAddr(IOAddress("ff02::1:2")); + pkt->setIndex(2); + pkt->setIface("eth0"); + + return (pkt); +} + +/// @brief a class dedicated to Hooks testing in DHCPv6 server +/// +/// This class has a number of static members, because each non-static +/// method has implicit 'this' parameter, so it does not match callout +/// signature and couldn't be registered. Furthermore, static methods +/// can't modify non-static members (for obvious reasons), so many +/// fields are declared static. It is still better to keep them as +/// one class rather than unrelated collection of global objects. +class HooksDhcpv6SrvTest : public Dhcpv6SrvTest { + +public: + + /// @brief creates Dhcpv6Srv and prepares buffers for callouts + HooksDhcpv6SrvTest() { + + // Allocate new DHCPv6 Server + srv_ = new NakedDhcpv6Srv(0); + + // clear static buffers + resetCalloutBuffers(); + } + + /// @brief destructor (deletes Dhcpv6Srv) + ~HooksDhcpv6SrvTest() { + delete srv_; + } + + /// @brief creates an option with specified option code + /// + /// This method is static, because it is used from callouts + /// that do not have a pointer to HooksDhcpv6SSrvTest object + /// + /// @param option_code code of option to be created + /// + /// @return pointer to create option object + static OptionPtr createOption(uint16_t option_code) { + + char payload[] = { + 0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14 + }; + + OptionBuffer tmp(payload, payload + sizeof(payload)); + return OptionPtr(new Option(Option::V6, option_code, tmp)); + } + + /// test callback that stores received callout name and pkt6 value + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_receive_callout(CalloutHandle& callout_handle) { + callback_name_ = string("pkt6_receive"); + + callout_handle.getArgument("query6", callback_pkt6_); + + callback_argument_names_ = callout_handle.getArgumentNames(); + return (0); + } + + /// test callback that changes client-id value + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_receive_change_clientid(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("query6", pkt); + + // get rid of the old client-id + pkt->delOption(D6O_CLIENTID); + + // add a new option + pkt->addOption(createOption(D6O_CLIENTID)); + + // carry on as usual + return pkt6_receive_callout(callout_handle); + } + + /// test callback that deletes client-id + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_receive_delete_clientid(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("query6", pkt); + + // get rid of the old client-id + pkt->delOption(D6O_CLIENTID); + + // carry on as usual + return pkt6_receive_callout(callout_handle); + } + + /// test callback that sets skip flag + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_receive_skip(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("query6", pkt); + + callout_handle.setSkip(true); + + // carry on as usual + return pkt6_receive_callout(callout_handle); + } + + /// Test callback that stores received callout name and pkt6 value + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_send_callout(CalloutHandle& callout_handle) { + callback_name_ = string("pkt6_send"); + + callout_handle.getArgument("response6", callback_pkt6_); + + callback_argument_names_ = callout_handle.getArgumentNames(); + return (0); + } + + // Test callback that changes server-id + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_send_change_serverid(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("response6", pkt); + + // get rid of the old server-id + pkt->delOption(D6O_SERVERID); + + // add a new option + pkt->addOption(createOption(D6O_SERVERID)); + + // carry on as usual + return pkt6_send_callout(callout_handle); + } + + /// test callback that deletes server-id + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_send_delete_serverid(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("response6", pkt); + + // get rid of the old client-id + pkt->delOption(D6O_SERVERID); + + // carry on as usual + return pkt6_send_callout(callout_handle); + } + + /// Test callback that sets skip flag + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + pkt6_send_skip(CalloutHandle& callout_handle) { + + Pkt6Ptr pkt; + callout_handle.getArgument("response6", pkt); + + callout_handle.setSkip(true); + + // carry on as usual + return pkt6_send_callout(callout_handle); + } + + /// Test callback that stores received callout name and subnet6 values + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + subnet6_select_callout(CalloutHandle& callout_handle) { + callback_name_ = string("subnet6_select"); + + callout_handle.getArgument("query6", callback_pkt6_); + callout_handle.getArgument("subnet6", callback_subnet6_); + callout_handle.getArgument("subnet6collection", callback_subnet6collection_); + + callback_argument_names_ = callout_handle.getArgumentNames(); + return (0); + } + + /// Test callback that picks the other subnet if possible. + /// @param callout_handle handle passed by the hooks framework + /// @return always 0 + static int + subnet6_select_different_subnet_callout(CalloutHandle& callout_handle) { + + // Call the basic calllout to record all passed values + subnet6_select_callout(callout_handle); + + const Subnet6Collection* subnets; + Subnet6Ptr subnet; + callout_handle.getArgument("subnet6", subnet); + callout_handle.getArgument("subnet6collection", subnets); + + // Let's change to a different subnet + if (subnets->size() > 1) { + subnet = (*subnets)[1]; // Let's pick the other subnet + callout_handle.setArgument("subnet6", subnet); + } + + return (0); + } + + /// resets buffers used to store data received by callouts + void resetCalloutBuffers() { + callback_name_ = string(""); + callback_pkt6_.reset(); + callback_subnet6_.reset(); + callback_subnet6collection_ = NULL; + callback_argument_names_.clear(); + } + + /// pointer to Dhcpv6Srv that is used in tests + NakedDhcpv6Srv* srv_; + + // The following fields are used in testing pkt6_receive_callout + + /// String name of the received callout + static string callback_name_; + + /// Pkt6 structure returned in the callout + static Pkt6Ptr callback_pkt6_; + + /// Pointer to a subnet received by callout + static Subnet6Ptr callback_subnet6_; + + /// A list of all available subnets (received by callout) + static const Subnet6Collection* callback_subnet6collection_; + + /// A list of all received arguments + static vector callback_argument_names_; +}; + +// The following fields are used in testing pkt6_receive_callout. +// See fields description in the class for details +string HooksDhcpv6SrvTest::callback_name_; +Pkt6Ptr HooksDhcpv6SrvTest::callback_pkt6_; +Subnet6Ptr HooksDhcpv6SrvTest::callback_subnet6_; +const Subnet6Collection* HooksDhcpv6SrvTest::callback_subnet6collection_; +vector HooksDhcpv6SrvTest::callback_argument_names_; + + +// Checks if callouts installed on pkt6_received are indeed called and the +// all necessary parameters are passed. +// +// Note that the test name does not follow test naming convention, +// but the proper hook name is "pkt6_receive". +TEST_F(HooksDhcpv6SrvTest, simple_pkt6_receive) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_receive", pkt6_receive_callout)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // check that the callback called is indeed the one we installed + EXPECT_EQ("pkt6_receive", callback_name_); + + // check that pkt6 argument passing was successful and returned proper value + EXPECT_TRUE(callback_pkt6_.get() == sol.get()); + + // Check that all expected parameters are there + vector expected_argument_names; + expected_argument_names.push_back(string("query6")); + + EXPECT_TRUE(expected_argument_names == callback_argument_names_); +} + +// Checks if callouts installed on pkt6_received is able to change +// the values and the parameters are indeed used by the server. +TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_receive) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_receive", pkt6_receive_change_clientid)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // check that the server did send a reposonce + ASSERT_EQ(1, srv_->fake_sent_.size()); + + // Make sure that we received a response + Pkt6Ptr adv = srv_->fake_sent_.front(); + ASSERT_TRUE(adv); + + // Get client-id... + OptionPtr clientid = adv->getOption(D6O_CLIENTID); + + // ... and check if it is the modified value + OptionPtr expected = createOption(D6O_CLIENTID); + EXPECT_TRUE(clientid->equal(expected)); +} + +// Checks if callouts installed on pkt6_received is able to delete +// existing options and that change impacts server processing (mandatory +// client-id option is deleted, so the packet is expected to be dropped) +TEST_F(HooksDhcpv6SrvTest, deleteClientId_pkt6_receive) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_receive", pkt6_receive_delete_clientid)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // Check that the server dropped the packet and did not send a response + ASSERT_EQ(0, srv_->fake_sent_.size()); +} + +// Checks if callouts installed on pkt6_received is able to set skip flag that +// will cause the server to not process the packet (drop), even though it is valid. +TEST_F(HooksDhcpv6SrvTest, skip_pkt6_receive) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_receive", pkt6_receive_skip)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // check that the server dropped the packet and did not produce any response + ASSERT_EQ(0, srv_->fake_sent_.size()); +} + + +// Checks if callouts installed on pkt6_send are indeed called and the +// all necessary parameters are passed. +TEST_F(HooksDhcpv6SrvTest, simple_pkt6_send) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_send", pkt6_send_callout)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // Check that the callback called is indeed the one we installed + EXPECT_EQ("pkt6_send", callback_name_); + + // Check that there is one packet sent + ASSERT_EQ(1, srv_->fake_sent_.size()); + Pkt6Ptr adv = srv_->fake_sent_.front(); + + // Check that pkt6 argument passing was successful and returned proper value + EXPECT_TRUE(callback_pkt6_.get() == adv.get()); + + // Check that all expected parameters are there + vector expected_argument_names; + expected_argument_names.push_back(string("response6")); + EXPECT_TRUE(expected_argument_names == callback_argument_names_); +} + +// Checks if callouts installed on pkt6_send is able to change +// the values and the packet sent contains those changes +TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_send) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_send", pkt6_send_change_serverid)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // check that the server did send a reposonce + ASSERT_EQ(1, srv_->fake_sent_.size()); + + // Make sure that we received a response + Pkt6Ptr adv = srv_->fake_sent_.front(); + ASSERT_TRUE(adv); + + // Get client-id... + OptionPtr clientid = adv->getOption(D6O_SERVERID); + + // ... and check if it is the modified value + OptionPtr expected = createOption(D6O_SERVERID); + EXPECT_TRUE(clientid->equal(expected)); +} + +// Checks if callouts installed on pkt6_send is able to delete +// existing options and that server applies those changes. In particular, +// we are trying to send a packet without server-id. The packet should +// be sent +TEST_F(HooksDhcpv6SrvTest, deleteServerId_pkt6_send) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_send", pkt6_send_delete_serverid)); + + // Let's create a simple SOLICIT + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // Check that the server indeed sent a malformed ADVERTISE + ASSERT_EQ(1, srv_->fake_sent_.size()); + + // Get that ADVERTISE + Pkt6Ptr adv = srv_->fake_sent_.front(); + ASSERT_TRUE(adv); + + // Make sure that it does not have server-id + EXPECT_FALSE(adv->getOption(D6O_SERVERID)); +} + +// Checks if callouts installed on pkt6_skip is able to set skip flag that +// will cause the server to not process the packet (drop), even though it is valid. +TEST_F(HooksDhcpv6SrvTest, skip_pkt6_send) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "pkt6_send", pkt6_send_skip)); + + // Let's create a simple REQUEST + Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit()); + + // Simulate that we have received that traffic + srv_->fakeReceive(sol); + + // Server will now process to run its normal loop, but instead of calling + // IfaceMgr::receive6(), it will read all packets from the list set by + // fakeReceive() + // In particular, it should call registered pkt6_receive callback. + srv_->run(); + + // check that the server dropped the packet and did not produce any response + ASSERT_EQ(0, srv_->fake_sent_.size()); +} + +// This test checks if subnet6_select callout is triggered and reports +// valid parameters +TEST_F(HooksDhcpv6SrvTest, subnet6_select) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "subnet6_select", subnet6_select_callout)); + + // Configure 2 subnets, both directly reachable over local interface + // (let's not complicate the matter with relays) + string config = "{ \"interfaces\": [ \"*\" ]," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ { " + " \"pool\": [ \"2001:db8:1::/64\" ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"" + valid_iface_ + "\" " + " }, {" + " \"pool\": [ \"2001:db8:2::/64\" ]," + " \"subnet\": \"2001:db8:2::/48\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ElementPtr json = Element::fromJSON(config); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + // Prepare solicit packet. Server should select first subnet for it + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("fe80::abcd")); + sol->setIface(valid_iface_); + sol->addOption(generateIA(234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // Pass it to the server and get an advertise + Pkt6Ptr adv = srv_->processSolicit(sol); + + // check if we get response at all + ASSERT_TRUE(adv); + + // Check that the callback called is indeed the one we installed + EXPECT_EQ("subnet6_select", callback_name_); + + // Check that pkt6 argument passing was successful and returned proper value + EXPECT_TRUE(callback_pkt6_.get() == sol.get()); + + const Subnet6Collection* exp_subnets = CfgMgr::instance().getSubnets6(); + + // The server is supposed to pick the first subnet, because of matching + // interface. Check that the value is reported properly. + ASSERT_TRUE(callback_subnet6_); + EXPECT_EQ(callback_subnet6_.get(), exp_subnets->front().get()); + + // Server is supposed to report two subnets + ASSERT_EQ(exp_subnets->size(), callback_subnet6collection_->size()); + + // Compare that the available subnets are reported as expected + EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet6collection_)[0].get()); + EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet6collection_)[1].get()); +} + +// This test checks if callout installed on subnet6_select hook point can pick +// a different subnet. +TEST_F(HooksDhcpv6SrvTest, subnet_select_change) { + + // Install pkt6_receive_callout + EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout( + "subnet6_select", subnet6_select_different_subnet_callout)); + + // Configure 2 subnets, both directly reachable over local interface + // (let's not complicate the matter with relays) + string config = "{ \"interfaces\": [ \"*\" ]," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ { " + " \"pool\": [ \"2001:db8:1::/64\" ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"interface\": \"" + valid_iface_ + "\" " + " }, {" + " \"pool\": [ \"2001:db8:2::/64\" ]," + " \"subnet\": \"2001:db8:2::/48\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ElementPtr json = Element::fromJSON(config); + ConstElementPtr status; + + // Configure the server and make sure the config is accepted + EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json)); + ASSERT_TRUE(status); + comment_ = parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + // Prepare solicit packet. Server should select first subnet for it + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("fe80::abcd")); + sol->setIface(valid_iface_); + sol->addOption(generateIA(234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // Pass it to the server and get an advertise + Pkt6Ptr adv = srv_->processSolicit(sol); + + // check if we get response at all + ASSERT_TRUE(adv); + + // The response should have an address from second pool, so let's check it + OptionPtr tmp = adv->getOption(D6O_IA_NA); + ASSERT_TRUE(tmp); + boost::shared_ptr ia = boost::dynamic_pointer_cast(tmp); + ASSERT_TRUE(ia); + tmp = ia->getOption(D6O_IAADDR); + ASSERT_TRUE(tmp); + boost::shared_ptr addr_opt = + boost::dynamic_pointer_cast(tmp); + ASSERT_TRUE(addr_opt); + + // Get all subnets and use second subnet for verification + const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6(); + ASSERT_EQ(2, subnets->size()); + + // Advertised address must belong to the second pool (in subnet's range, + // in dynamic pool) + EXPECT_TRUE((*subnets)[1]->inRange(addr_opt->getAddress())); + EXPECT_TRUE((*subnets)[1]->inPool(addr_opt->getAddress())); +} + +} // end of anonymous namespace