diff options
Diffstat (limited to 'src/bin/dhcp6/tests/shared_network_unittest.cc')
-rw-r--r-- | src/bin/dhcp6/tests/shared_network_unittest.cc | 2916 |
1 files changed, 2916 insertions, 0 deletions
diff --git a/src/bin/dhcp6/tests/shared_network_unittest.cc b/src/bin/dhcp6/tests/shared_network_unittest.cc new file mode 100644 index 0000000..714102f --- /dev/null +++ b/src/bin/dhcp6/tests/shared_network_unittest.cc @@ -0,0 +1,2916 @@ +// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/io_address.h> +#include <dhcp/option.h> +#include <dhcp/option_int.h> +#include <dhcp/option6_client_fqdn.h> +#include <dhcp/option6_addrlst.h> +#include <dhcp/tests/iface_mgr_test_config.h> +#include <dhcpsrv/cfg_subnets6.h> +#include <dhcpsrv/cfgmgr.h> +#include <dhcpsrv/lease_mgr_factory.h> +#include <dhcp6/json_config_parser.h> +#include <dhcp6/tests/dhcp6_client.h> +#include <dhcp6/tests/dhcp6_test_utils.h> +#include <cc/command_interpreter.h> +#include <stats/stats_mgr.h> +#include <boost/pointer_cast.hpp> +#include <functional> +#include <vector> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::dhcp; +using namespace isc::dhcp::test; +using namespace isc::stats; + +namespace { + +/// @brief Array of server configurations used throughout the tests. +const char* NETWORKS_CONFIG[] = { +// Configuration #0. +// - one shared network with two subnets, each with address and prefix pools +// - one plain subnet + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"comment\": \"example\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]," + " \"pd-pools\": [" + " {" + " \"prefix\": \"4000::\"," + " \"prefix-len\": 96," + " \"delegated-len\": 96" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]," + " \"pd-pools\": [" + " {" + " \"prefix\": \"5000::\"," + " \"prefix-len\": 96," + " \"delegated-len\": 96" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"3000::/96\"," + " \"id\": 1000," + " \"interface\": \"eth0\"," + " \"pools\": [" + " {" + " \"pool\": \"3000::1 - 3000::1\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #1. +// - one shared network with relay-ip specified and one subnet with address pool +// - one plain subnet with relay-ip specified and one address pool + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"relay\": {" + " \"ip-address\": \"3001::1\"" + " }," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 1000," + " \"relay\": {" + " \"ip-address\": \"3001::2\"" + " }," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #2. +// - two classes specified +// - one shared network with two subnets (the first has class restrictions) + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #3. +// - two classes defined +// - one shared network with two subnets, each with a different class restriction + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #4. +// - one shared network with two subnets. Each subnet has: +// - address and prefix pool +// - reservation + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"pd-pools\": [" + " {" + " \"prefix\": \"4000::\"," + " \"prefix-len\": 96," + " \"delegated-len\": 96" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::1 - 2001:db8:2::64\"" + " }" + " ]," + " \"pd-pools\": [" + " {" + " \"prefix\": \"5000::\"," + " \"prefix-len\": 96," + " \"delegated-len\": 112" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:11:22:33:44:55:66\"," + " \"ip-addresses\": [ \"2001:db8:2::28\" ]," + " \"prefixes\": [ \"5000::8:0000/112\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #5 (similar to #4, but without prefix pool and using different +// DUID for reservations) + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:11:22:33:44:55:66\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::1 - 2001:db8:2::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:2::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #6. +// - one class +// - one shared network with two subnets: +// - first subnet has address pool, class restriction and a reservation +// - second subnet has just an address pool + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"client-class\": \"a-devices\"," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::16 - 2001:db8:2::16\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #7. +// - option defined on global level +// - one shared network with two options and two subnets +// - the first subnet has its own options defined as well +// - plain subnet with its own options + "{" + " \"option-data\": [" + " {" + " \"name\": \"nis-servers\"," + " \"data\": \"3000::20\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"3001::21\"" + " }," + " {" + " \"name\": \"nisp-servers\"," + " \"data\": \"3002::34\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"option-data\": [" + " {" + " \"name\": \"sntp-servers\"," + " \"data\": \"4004::22\"" + " }," + " {" + " \"name\": \"nisp-servers\"," + " \"data\": \"3003::33\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " \{" + " \"subnet\": \"3000::/96\"," + " \"id\": 1000," + " \"interface\": \"eth0\"," + " \"option-data\": [" + " {" + " \"name\": \"nisp-servers\"," + " \"data\": \"4000::5\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"3000::1 - 3000::1\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #8. +// - two shared networks +// - first network with two subnets, each with its own address pool +// - second network with two subnets, each with its own address pool + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }," + " {" + " \"name\": \"dog\"," + " \"interface\": \"eth0\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:3::/64\"," + " \"id\": 1000," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:3::20 - 2001:db8:3::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:4::/64\"," + " \"id\": 10000," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:4::20 - 2001:db8:4::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #9 (similar to #8, but with relay-ip addresses specified) +// - two shared networks, each with relay IP addresses specified + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"relay\": {" + " \"ip-address\": \"3000::1\"" + " }," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }," + " {" + " \"name\": \"dog\"," + " \"relay\": {" + " \"ip-address\": \"3000::2\"" + " }," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:3::/64\"," + " \"id\": 1000," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:3::20 - 2001:db8:3::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:4::/64\"," + " \"id\": 10000," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:4::20 - 2001:db8:4::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #10. +// - one class with an option (and not test expression) +// - one shared network with two subnets +// - first subnet with one address pool +// - second with a pool and reservation that assigns client to a class + "{" + " \"client-classes\": [" + " {" + " \"name\": \"class-with-dns-servers\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::50\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::30\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:11:22:33:44:55:66\"," + " \"ip-addresses\": [ \"2001:db8:2::20\" ]," + " \"hostname\": \"test.example.org\"," + " \"client-classes\": [ \"class-with-dns-servers\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #11. +// - two classes defined +// - two shared networks, each with one subnet and class restriction + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"client-class\": \"a-devices\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }" + " ]" + " }," + " {" + " \"name\": \"dog\"," + " \"interface\": \"eth1\"," + " \"client-class\": \"b-devices\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 1000," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #12. +// - one client class +// - one shared network with two subnets, the second subnet has class restriction + "{" + " \"client-classes\": [" + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #13. +// - one shared network, with two subnets, each with the same relay-ip addresses +// - one plain subnet, with its own (different) relay-ip address + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"relay\": {" + " \"ip-address\": \"3001::1\"" + " }," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"relay\": {" + " \"ip-address\": \"3001::1\"" + " }," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:3::/64\"," + " \"id\": 1000," + " \"relay\": {" + " \"ip-address\": \"3001::2\"" + " }," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:3::20 - 2001:db8:3::20\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #14. +// - one share network with interface-id specified and one subnet +// - one plain subnet, with its own interface-id + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface-id\": \"vlan10\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 1000," + " \"interface-id\": \"vlan1000\"," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #15. +// - one shared network, with two subnets, each with the same interface-id +// - one plain subnet, with its own interface-id + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"interface-id\": \"vlan10\"," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 11," + " \"interface-id\": \"vlan10\"," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::10 - 2001:db8:2::10\"" + " }" + " ]" + " }" + " ]" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:2::1/64\"," + " \"id\": 1000," + " \"interface-id\": \"vlan1000\"," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #16. +// - one shared network with three subnets, each with different option value + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"4004::22\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"5555::33\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:3::/64\"," + " \"id\": 1000," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"1234::23\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:3::20 - 2001:db8:3::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #17. +// - one shared network with two subnets, both have rapid-commit enabled + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"rapid-commit\": true," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"rapid-commit\": true," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + + +// Configuration #18. +// - one shared network with rapid-commit enabled +// - two subnets (which should derive the rapid-commit setting) + "{" + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"rapid-commit\": true," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"" + " }" + " ]" + " }," + " {" + " \"subnet\": \"2001:db8:2::/64\"," + " \"id\": 100," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:2::20 - 2001:db8:2::20\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #19. +// - one shared network with one subnet and two pools (the first has +// class restrictions) + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"2001:db8:1::50 - 2001:db8:1::50\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #20. +// - one shared network with one subnet and two pools (each with class +// restriction) + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"2001:db8:1::50 - 2001:db8:1::50\"," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #21. +// - one plain subnet with two pools (the first has class restrictions) + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"interface\": \"eth1\"," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"2001:db8:1::50 - 2001:db8:1::50\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #22. +// - one plain subnet with two pools (each with class restriction) + "{" + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[1234].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[1234].hex == 0x0002\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::20 - 2001:db8:1::20\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"2001:db8:1::50 - 2001:db8:1::50\"," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}" + +}; + +/// @Brief Test fixture class for DHCPv6 server using shared networks. +class Dhcpv6SharedNetworkTest : public Dhcpv6SrvTest { +public: + + /// @brief Indicates how test functions should check presence of a lease on + /// the server. + enum class LeaseOnServer{ + MUST_EXIST, + MUST_NOT_EXIST, + }; + + /// @brief Constructor. + Dhcpv6SharedNetworkTest() + : Dhcpv6SrvTest(), + iface_mgr_test_config_(true) { + IfaceMgr::instance().openSockets6(); + StatsMgr::instance().removeAll(); + } + + /// @brief Verifies lease statistics against values held by StatsMgr. + /// + /// This method retrieves lease statistics from the database and then compares it + /// against values held by the StatsMgr. The compared statistics are number of + /// assigned addresses and prefixes for a subnet. + void verifyAssignedStats() { + LeaseStatsQueryPtr query = LeaseMgrFactory::instance().startLeaseStatsQuery6(); + LeaseStatsRow row; + while (query->getNextRow(row)) { + // Only check valid leases. + if (row.lease_state_ == Lease::STATE_DEFAULT) { + std::string stat_name; + // Addresses + if (row.lease_type_ == Lease::TYPE_NA) { + stat_name = StatsMgr::generateName("subnet", row.subnet_id_, + "assigned-nas"); + // Prefixes. + } else if (row.lease_type_ == Lease::TYPE_PD) { + stat_name = StatsMgr::generateName("subnet", row.subnet_id_, + "assigned-pds"); + } + + // Number of leases held in the database should match the information + // held in the Stats Manager. + if (!stat_name.empty()) { + ASSERT_EQ(row.state_count_, getStatsAssignedLeases(stat_name)) + << "test failed for statistic " << stat_name; + } + } + } + } + + /// @brief Retrieves statistics for a subnet. + /// + /// @param stat_name Name of the statistics to be retrieved, e.g. subnet[1234].assigned-nas. + /// @return Number of assigned leases for a subnet. + int64_t getStatsAssignedLeases(const std::string& stat_name) const { + // Top element is a map with a subnet[id].assigned-addresses parameter. + ConstElementPtr top_element = StatsMgr::instance().get(stat_name); + if (top_element && (top_element->getType() == Element::map)) { + // It contains two lists (nested). + ConstElementPtr first_list = top_element->get(stat_name); + if (first_list && (first_list->getType() == Element::list) && + (first_list->size() > 0)) { + // Get the nested list which should have two elements, of which first + // is the statistics value we're looking for. + ConstElementPtr second_list = first_list->get(0); + if (second_list && (second_list->getType() == Element::list)) { + ConstElementPtr addresses_element = second_list->get(0); + if (addresses_element && (addresses_element->getType() == Element::integer)) { + return (addresses_element->intValue()); + } + } + } + } + + // Statistics invalid or not found. + return (0); + } + + /// @brief Launches specific operation and verifies lease statistics before and + /// after this operation. + /// + /// @param operation Operation to be launched. + void testAssigned(const std::function<void()>& operation) { + ASSERT_NO_FATAL_FAILURE(verifyAssignedStats()); + operation(); + ASSERT_NO_FATAL_FAILURE(verifyAssignedStats()); + } + + /// @brief Returns subnet having specified address or prefix in range. + /// + /// @param type Resource type: NA or PD. + /// @param resource Address or prefix for which subnet is being searched. + /// @return Pointer to the subnet having an resource in range or null pointer + /// if no subnet found. + Subnet6Ptr getConfiguredSubnet(const Lease::Type& type, const IOAddress& resource) const { + CfgSubnets6Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSubnets6(); + const Subnet6Collection* subnets = cfg->getAll(); + for (auto subnet_it = subnets->cbegin(); subnet_it != subnets->cend(); ++subnet_it) { + if ((*subnet_it)->inPool(type, resource)) { + return (*subnet_it); + } + } + + return (Subnet6Ptr()); + + } + + /// @brief Check if client has a lease for the specified address. + /// + /// Apart from checking whether the client has got the lease it also + /// checks whether this lease is stored in the lease database and that + /// it holds valid subnet identifier. + /// + /// @param client Reference to the client. + /// @param address Leased address. + /// @param lease_on_server Specify whether the lease should be also present or + /// absent in the lease database. + /// + /// @return true if the lease for the client has been found both in the + /// database and in the server's response. + bool hasLeaseForAddress(Dhcp6Client& client, const IOAddress& address, + const LeaseOnServer& lease_on_server = LeaseOnServer::MUST_EXIST) { + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, address); + // Sanity check the lease. + if (lease) { + Subnet6Ptr subnet = getConfiguredSubnet(Lease::TYPE_NA, address); + if (!subnet) { + ADD_FAILURE() << "unable to find configured subnet for the" + " address " << address; + return (false); + } + // Make sure that the subnet id is not messed up in the lease. + if (subnet->getID() != lease->subnet_id_) { + ADD_FAILURE() << "invalid subnet identifier found in the lease for" + " address " << address << ", expected " << subnet->getID() + << ", got " << lease->subnet_id_; + return (false); + } + } + return ((((lease_on_server == LeaseOnServer::MUST_EXIST) && lease) || + ((lease_on_server == LeaseOnServer::MUST_NOT_EXIST) && !lease)) && + client.hasLeaseForAddress(address)); + } + + /// @brief Check if client has a lease for the specified prefix. + /// + /// Apart from checking whether the client has got the lease it also + /// checks whether this lease is stored in the lease database and that + /// it holds valid subnet identifier. + /// + /// @param client Reference to the client. + /// @param prefix Leased prefix. + /// @param prefix_len Leased prefix length. + /// @param lease_on_server Specify whether the lease should be also present or + /// absent in the lease database. + /// + /// @return true if the lease for the client has been found both in the + /// database and in the server's response. + bool hasLeaseForPrefix(Dhcp6Client& client, const IOAddress& prefix, + const uint8_t prefix_len, const IAID& iaid, + const LeaseOnServer& lease_on_server = LeaseOnServer::MUST_EXIST) { + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_PD, prefix); + + // Sanity check the lease. + if (lease) { + Subnet6Ptr subnet = getConfiguredSubnet(Lease::TYPE_PD, prefix); + if (!subnet) { + ADD_FAILURE() << "unable to find configured subnet for the" + " prefix " << prefix; + return (false); + } + // Make sure that the subnet id is not messed up in the lease. + if (subnet->getID() != lease->subnet_id_) { + ADD_FAILURE() << "invalid subnet identifier found in the lease for" + " prefix " << prefix; + return (false); + } + } + + return ((((lease_on_server == LeaseOnServer::MUST_EXIST) && lease && + (lease->prefixlen_ = prefix_len) && (lease->iaid_ == iaid)) || + ((lease_on_server == LeaseOnServer::MUST_NOT_EXIST) && !lease)) && + client.hasLeaseForPrefix(prefix, prefix_len, iaid)); + } + + /// @brief Check if client has a lease belonging to address range. + /// + /// Apart from checking whether the client has got the lease it also + /// checks whether this lease is stored in the lease database. + /// + /// @param client Reference to the client. + /// @param first Lower bound of the address range. + /// @param last Upper bound of the address range. + /// @param lease_on_server Specify whether the lease should be also present or + /// absent in the lease database. + bool hasLeaseForAddressRange(Dhcp6Client& client, const IOAddress& first, const IOAddress& last, + const LeaseOnServer& lease_on_server = LeaseOnServer::MUST_EXIST) { + std::vector<Lease6> leases = client.getLeasesByAddressRange(first, last); + for (auto lease_it = leases.cbegin(); lease_it != leases.cend(); ++lease_it) { + // Take into account only valid leases. + if (lease_it->valid_lft_ == 0) { + continue; + } + + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, lease_it->addr_); + if ((lease && (lease_on_server == LeaseOnServer::MUST_NOT_EXIST)) || + (!lease && (lease_on_server == LeaseOnServer::MUST_EXIST))) { + return (false); + } + } + + return (!leases.empty()); + } + + /// @brief Check if client has a lease belonging to a prefix pool. + /// + /// Apart from checking whether the client has got the lease it also + /// checks whether this lease is stored in the lease database. + /// + /// @param client Reference to the client. + /// @param prefix Pool prefix. + /// @param prefix_len Prefix length. + /// @param delegated_len Delegated prefix length. + /// @param lease_on_server Specify whether the lease should be also present or + /// absent in the lease database. + /// + /// @return true if client has a lease belonging to specified pool, + /// false otherwise. + bool hasLeaseForPrefixPool(Dhcp6Client& client, const asiolink::IOAddress& prefix, + const uint8_t prefix_len, const uint8_t delegated_len, + const LeaseOnServer& lease_on_server = LeaseOnServer::MUST_EXIST) { + std::vector<Lease6> leases = client.getLeasesByPrefixPool(prefix, prefix_len, delegated_len); + + for (auto lease_it = leases.cbegin(); lease_it != leases.cend(); ++lease_it) { + // Take into account only valid leases. + if (lease_it->valid_lft_ == 0) { + continue; + } + + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_PD, lease_it->addr_); + if ((lease && (lease->prefixlen_ == lease->prefixlen_) && + (lease_on_server == LeaseOnServer::MUST_NOT_EXIST)) || + (!lease && (lease_on_server == LeaseOnServer::MUST_EXIST))) { + return (false); + } + } + + return (!leases.empty()); + + } + + /// @brief Tests that for a given configuration the rapid-commit works (or not) + /// + /// The provided configuration is expected to be able to handle two clients. + /// The second parameter governs whether rapid-commit is expected to be enabled + /// or disabled. Third and fourth parameters are text representations of expected + /// leases to be assigned (if rapid-commit is enabled) + /// + /// @param config - text version of the configuration to be tested + /// @param enabled - true = rapid-commit is expected to work + /// @param exp_addr1 - an address the first client is expected to get (if + /// rapid-commit is enabled). + /// @param exp_addr2 - an address the second client is expected to get (if + /// rapid-commit is enabled). + void testRapidCommit(const std::string& config, bool enabled, + const std::string& exp_addr1, + const std::string& exp_addr2) { + + // Create client #1. This clients wants to use rapid-commit. + Dhcp6Client client1; + client1.setInterface("eth1"); + client1.useRapidCommit(true); + + Dhcp6Client client2; + client2.setInterface("eth1"); + client2.useRapidCommit(true); + + // Configure the server with a shared network. + ASSERT_NO_FATAL_FAILURE(configure(config, *client1.getServer())); + + // Ok, client should have one + EXPECT_EQ(0, client1.getLeaseNum()); + + // Client #1 should be assigned an address from shared network. The first + // subnet has rapid-commit enabled, so the address should be assigned. + // We provide a hint for this allocation to make sure that the address + // from the first subnet is allocated. In theory, an address from the + // second subnet could be allocated as well if the hint was not provided. + IOAddress requested_address = exp_addr1.empty() ? IOAddress::IPV6_ZERO_ADDRESS() : + IOAddress(exp_addr1); + ASSERT_NO_THROW(client1.requestAddress(0xabca0, requested_address)); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSolicit()); + }); + + // Make sure something was sent back. + ASSERT_TRUE(client1.getContext().response_); + + if (enabled) { + // rapid-commit enabled. + + // Make sure that REPLY was sent back. + EXPECT_EQ(DHCPV6_REPLY, client1.getContext().response_->getType()); + + // Just make sure the client didn't get an address. + EXPECT_TRUE(hasLeaseForAddress(client1, IOAddress(exp_addr1), + LeaseOnServer::MUST_EXIST)); + } else { + // rapid-commit disabled. + + // Make sure that ADVERTISE was sent back. + EXPECT_EQ(DHCPV6_ADVERTISE, client1.getContext().response_->getType()); + + // And that it doesn't have any leases. + EXPECT_EQ(0, client1.getLeaseNum()); + } + + // Create client #2. This client behaves the same as the first one, but the + // first subnet is already full (it's a really small subnet) and the second + // subnet does not allow rapid-commit. + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSolicit()); + }); + + // Make sure something was sent back. + ASSERT_TRUE(client2.getContext().response_); + + if (enabled) { + // rapid-commit enabled. + + // Make sure that REPLY was sent back. + EXPECT_EQ(DHCPV6_REPLY, client2.getContext().response_->getType()); + + // Just make sure the client didn't get an address. + EXPECT_TRUE(hasLeaseForAddress(client2, IOAddress(exp_addr2), + LeaseOnServer::MUST_EXIST)); + } else { + // rapid-commit disabled. + + // Make sure that ADVERTISE was sent back. + EXPECT_EQ(DHCPV6_ADVERTISE, client1.getContext().response_->getType()); + + // And that it doesn't have any leases. + EXPECT_EQ(0, client1.getLeaseNum()); + } + } + + /// @brief Check precedence. + /// + /// @param config the configuration. + /// @param ns_address expected name server address. + void testPrecedence(const std::string& config, const std::string& ns_address) { + // Create client and set DUID to the one that has a reservation. + Dhcp6Client client; + client.setInterface("eth1"); + client.setDUID("00:03:00:01:aa:bb:cc:dd:ee:ff"); + client.requestAddress(0xabca, IOAddress("2001:db8:1::28")); + // Request dns-servers. + client.requestOption(D6O_NAME_SERVERS); + + // Create server configuration. + configure(config, *client.getServer()); + + // Perform SARR. + ASSERT_NO_THROW(client.doSARR()); + + // Check response. + EXPECT_EQ(1, client.getLeaseNum()); + Pkt6Ptr resp = client.getContext().response_; + ASSERT_TRUE(resp); + + // Check dns-servers option. + OptionPtr opt = resp->getOption(D6O_NAME_SERVERS); + ASSERT_TRUE(opt); + Option6AddrLstPtr servers = + boost::dynamic_pointer_cast<Option6AddrLst>(opt); + ASSERT_TRUE(servers); + auto addrs = servers->getAddresses(); + ASSERT_EQ(1, addrs.size()); + EXPECT_EQ(ns_address, addrs[0].toText()); + } + + /// @brief Destructor. + virtual ~Dhcpv6SharedNetworkTest() { + StatsMgr::instance().removeAll(); + } + + /// @brief Interface Manager's fake configuration control. + IfaceMgrTestConfig iface_mgr_test_config_; +}; + +// Check user-context parsing +TEST_F(Dhcpv6SharedNetworkTest, parse) { + // Create client + Dhcp6Client client1; + + // Don't use configure from utils + Parser6Context ctx; + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP6(NETWORKS_CONFIG[0], true)); + ConstElementPtr status; + disableIfacesReDetect(json); + EXPECT_NO_THROW(status = configureDhcp6Server(*client1.getServer(), json)); + ASSERT_TRUE(status); + int rcode; + ConstElementPtr comment = config::parseAnswer(rcode, status); + ASSERT_EQ(0, rcode); + CfgMgr::instance().commit(); + + CfgSharedNetworks6Ptr cfg = CfgMgr::instance().getCurrentCfg()->getCfgSharedNetworks6(); + SharedNetwork6Ptr network = cfg->getByName("frog"); + ConstElementPtr context = network->getContext(); + ASSERT_TRUE(context); + ASSERT_EQ(1, context->size()); + ASSERT_TRUE(context->get("comment")); + EXPECT_EQ("\"example\"", context->get("comment")->str()); +} + +// Running out of addresses within a subnet in a shared network. +TEST_F(Dhcpv6SharedNetworkTest, addressPoolInSharedNetworkShortage) { + // Create client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + + // Configure the server with one shared network including two subnets and + // one subnet outside of the shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[0], *client1.getServer())); + + // Client #1 requests an address in first subnet within a shared network. + ASSERT_NO_THROW(client1.requestAddress(0xabca0, IOAddress("2001:db8:1::20"))); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Client #2 The second client will request a lease and should be assigned + // an address from the second subnet. + Dhcp6Client client2(client1.getServer()); + client2.setInterface("eth1"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); + + // Client #3. It sends Solicit which should result in NoAddrsAvail status + // code because all addresses available for this link have been assigned. + Dhcp6Client client3(client1.getServer()); + client3.setInterface("eth1"); + ASSERT_NO_THROW(client3.requestAddress(0xabca0)); + testAssigned([&client3] { + ASSERT_NO_THROW(client3.doSolicit(true)); + }); + EXPECT_EQ(0, client3.getLeaseNum()); + + // Client #3 should be assigned an address if subnet 3 is selected for it. + client3.setInterface("eth0"); + testAssigned([&client3] { + ASSERT_NO_THROW(client3.doSolicit(true)); + }); + EXPECT_EQ(1, client3.getLeaseNum()); + + // Client #1 should be able to renew its lease. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRenew()); + }); + EXPECT_EQ(1, client1.getLeaseNum()); + EXPECT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Client #2 should be able to renew its lease too. + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(1, client2.getLeaseNum()); + EXPECT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); +} + +// Shared network is selected based on relay link address. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectedByRelay) { + // Create client #1. This is a relayed client which is using relay address + // matching configured shared network. + Dhcp6Client client1; + client1.useRelay(true, IOAddress("3001::1")); + + // Configure the server with one shared network and one subnet outside of the + // shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[1], *client1.getServer())); + + // Client #1 should be assigned an address from shared network. + ASSERT_NO_THROW(client1.requestAddress(0xabca0)); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Create client #2. This is a relayed client which is using relay + // address matching subnet outside of the shared network. + Dhcp6Client client2(client1.getServer()); + client2.useRelay(true, IOAddress("3001::2")); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); +} + +// Providing a hint for any address belonging to a shared network. +TEST_F(Dhcpv6SharedNetworkTest, hintWithinSharedNetwork) { + // Create client #1. + Dhcp6Client client; + client.setInterface("eth1"); + + // Configure the server with one shared network including two subnets and + // one subnet outside of the shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[0], *client.getServer())); + + // Provide a hint to an existing address within first subnet. This address + // should be offered out of this subnet. + ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + testAssigned([&client] { + ASSERT_NO_THROW(client.doSolicit(true)); + }); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::20"), + LeaseOnServer::MUST_NOT_EXIST)); + + // Similarly, we should be offered an address from another subnet within + // the same shared network when we ask for it. + client.clearRequestedIAs(); + ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("2001:db8:2::20"))); + testAssigned([&client] { + ASSERT_NO_THROW(client.doSolicit(true)); + }); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::20"), + LeaseOnServer::MUST_NOT_EXIST)); + + // Asking for an address that is not in address pool should result in getting + // an address from one of the subnets, but generally hard to tell from which one. + client.clearRequestedIAs(); + ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("3002::123"))); + testAssigned([&client] { + ASSERT_NO_THROW(client.doSolicit(true)); + }); + std::vector<Lease6> leases = client.getLeasesByType(Lease::TYPE_NA); + ASSERT_EQ(1, leases.size()); + if (!hasLeaseForAddress(client, IOAddress("2001:db8:1::20"), + LeaseOnServer::MUST_NOT_EXIST) && + !hasLeaseForAddress(client, IOAddress("2001:db8:2::20"), + LeaseOnServer::MUST_NOT_EXIST)) { + ADD_FAILURE() << "Unexpected address advertised by the server " << leases.at(0).addr_; + } +} + +// Shared network is selected based on the client class specified. +TEST_F(Dhcpv6SharedNetworkTest, subnetInSharedNetworkSelectedByClass) { + // Create client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + + // Configure the server with one shared network including two subnets and + // one subnet outside of the shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[2], *client1.getServer())); + + // Client #1 requests an address in the restricted subnet but can't be assigned + // this address because the client doesn't belong to a certain class. + ASSERT_NO_THROW(client1.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:2::20"))); + + // Release the lease that the client has got, because we'll need this address + // further in the test. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRelease()); + }); + + // Add option 1234 which would cause the client to be classified as "a-devices". + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0001)); + client1.addExtraOption(option1234); + + // This time, the allocation of the address provided as hint should be successful. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Client 2 should be assigned an address from the unrestricted subnet. + Dhcp6Client client2(client1.getServer()); + client2.setInterface("eth1"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); + + // Now, let's reconfigure the server to also apply restrictions on the + // subnet to which client2 now belongs. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[3], *client1.getServer())); + + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(0, client2.getLeaseNum()); + + // If we add option 1234 with a value matching this class, the lease should + // get renewed. + OptionPtr option1234_bis(new OptionUint16(Option::V6, 1234, 0x0002)); + client2.addExtraOption(option1234_bis); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(1, client2.getLeaseNum()); +} + +// IPv6 address reservation exists in one of the subnets within shared network. +TEST_F(Dhcpv6SharedNetworkTest, reservationInSharedNetwork) { + // Create client #1. Explicitly set client's DUID to the one that has a + // reservation in the second subnet within shared network. + Dhcp6Client client1; + client1.setInterface("eth1"); + client1.setDUID("00:03:00:01:11:22:33:44:55:66"); + + // Create server configuration with a shared network including two subnets. There + // is an IP address reservation in each subnet for two respective clients. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[4], *client1.getServer())); + + // Client #1 should get his reserved address from the second subnet. + ASSERT_NO_THROW(client1.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:2::28"))); + + // Create client #2. + Dhcp6Client client2; + client2.setInterface("eth1"); + client2.setDUID("00:03:00:01:aa:bb:cc:dd:ee:ff"); + + // Client #2 should get its reserved address from the first subnet. + ASSERT_NO_THROW(client2.requestAddress(0xabca, IOAddress("2001:db8:1::30"))); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:1::28"))); + + // Reconfigure the server. Now, the first client get's second client's + // reservation and vice versa. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[5], *client1.getServer(), true, true, false)); + + // The first client is trying to renew the lease but should get a different lease + // because its lease is now reserved for some other client. The client won't be + // assigned a lease for which it has a reservation because another client holds + // this lease. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRenew()); + }); + ASSERT_TRUE(client1.hasLeaseWithZeroLifetimeForAddress(IOAddress("2001:db8:2::28"))); + ASSERT_FALSE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::28"))); + + // The client should be allocated a lease from one of the dynamic pools. + if (!hasLeaseForAddressRange(client1, IOAddress("2001:db8:2::1"), IOAddress("2001:db8:2::64")) && + !hasLeaseForAddressRange(client1, IOAddress("2001:db8:1::1"), IOAddress("2001:db8:1::64"))) { + ADD_FAILURE() << "unexpected lease allocated for renewing client"; + } + + // Client #2 is now renewing its lease and should get its newly reserved address. + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + ASSERT_TRUE(client2.hasLeaseWithZeroLifetimeForAddress(IOAddress("2001:db8:1::28"))); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::28"))); + + // Same for client #1. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRenew()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::28"))); +} + +// Reserved address can't be assigned as long as access to a subnet is +// restricted by classification. +TEST_F(Dhcpv6SharedNetworkTest, reservationAccessRestrictedByClass) { + // Create client #1. Explicitly set client's DUID to the one that has a + // reservation in the firstsubnet within shared network. + Dhcp6Client client; + client.setInterface("eth1"); + client.setDUID("00:03:00:01:aa:bb:cc:dd:ee:ff"); + + // Create server configuration with a shared network including two subnets. Access to + // one of the subnets is restricted by client classification. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[6], *client.getServer())); + + // Assigned address should be allocated from the second subnet, because the + // client doesn't belong to the "a-devices" class. + ASSERT_NO_THROW(client.requestAddress(0xabca)); + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::16"))); + + // Add option 1234 which would cause the client to be classified as "a-devices". + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0001)); + client.addExtraOption(option1234); + + // The client should now be assigned the reserved address from the first subnet. + testAssigned([&client] { + ASSERT_NO_THROW(client.doRenew()); + }); + ASSERT_TRUE(client.hasLeaseWithZeroLifetimeForAddress(IOAddress("2001:db8:2::16"))); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::28"))); +} + +// Subnet in which the client is renewing an address is restricted by classification. +TEST_F(Dhcpv6SharedNetworkTest, renewalRestrictedByClass) { + // Create client. + Dhcp6Client client; + client.setInterface("eth1"); + + // Create server configuration with a shared network including two subnets. Access to + // the second subnet is restricted by client classification. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[12], *client.getServer())); + + // Add option 1234 to cause the client to belong to the class. + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0002)); + client.addExtraOption(option1234); + + // Client requests an address from the second subnet which should be successful. + ASSERT_NO_THROW(client.requestAddress(0xabca, IOAddress("2001:db8:2::20"))); + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::20"))); + + // Now remove the client from this class. + client.clearExtraOptions(); + + // The client should not be able to renew the existing lease because it is now + // prohibited by the classification. Instead, the client should get a lease from the + // unrestricted subnet. + testAssigned([&client] { + ASSERT_NO_THROW(client.doRenew()); + }); + ASSERT_TRUE(client.hasLeaseWithZeroLifetimeForAddress(IOAddress("2001:db8:2::20"))); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::20"))); +} + +// Some options are specified on the shared subnet level, some on the +// subnets level. +TEST_F(Dhcpv6SharedNetworkTest, optionsDerivation) { + // Client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[7], *client1.getServer())); + + // Client #1 belongs to shared network. By providing a hint "2001:db8:1::20 we force + // the server to select first subnet within the shared network for this client. + ASSERT_NO_THROW(client1.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + + // Request all configured options. + ASSERT_NO_THROW(client1.requestOption(D6O_NIS_SERVERS)); + ASSERT_NO_THROW(client1.requestOption(D6O_NISP_SERVERS)); + ASSERT_NO_THROW(client1.requestOption(D6O_NAME_SERVERS)); + ASSERT_NO_THROW(client1.requestOption(D6O_SNTP_SERVERS)); + + // Perform 4-way exchange and make sure we have been assigned address from the + // subnet we wanted. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // This option is specified on the global level. + ASSERT_TRUE(client1.hasOptionWithAddress(D6O_NIS_SERVERS, "3000::20")); + + // Subnet specific value should override a value specified on the shared network level. + ASSERT_TRUE(client1.hasOptionWithAddress(D6O_NISP_SERVERS, "3003::33")); + + // Shared network level value should be derived to the subnet. + ASSERT_TRUE(client1.hasOptionWithAddress(D6O_NAME_SERVERS, "3001::21")); + + // This option is only specified in the subnet level. + ASSERT_TRUE(client1.hasOptionWithAddress(D6O_SNTP_SERVERS, "4004::22")); + + // Client #2. + Dhcp6Client client2(client1.getServer()); + client2.setInterface("eth1"); + + // Request an address from the second subnet within the shared network. + ASSERT_NO_THROW(client2.requestAddress(0xabca, IOAddress("2001:db8:2::20"))); + + // Request all configured options. + ASSERT_NO_THROW(client2.requestOption(D6O_NIS_SERVERS)); + ASSERT_NO_THROW(client2.requestOption(D6O_NISP_SERVERS)); + ASSERT_NO_THROW(client2.requestOption(D6O_NAME_SERVERS)); + ASSERT_NO_THROW(client2.requestOption(D6O_SNTP_SERVERS)); + + // Perform 4-way exchange and make sure we have been assigned address from the + // subnet we wanted. + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); + + // This option is specified on the global level. + ASSERT_TRUE(client2.hasOptionWithAddress(D6O_NIS_SERVERS, "3000::20")); + + // Shared network level value should be derived to the subnet. + ASSERT_TRUE(client2.hasOptionWithAddress(D6O_NAME_SERVERS, "3001::21")); + ASSERT_TRUE(client2.hasOptionWithAddress(D6O_NISP_SERVERS, "3002::34")); + + // Client #3. + Dhcp6Client client3(client1.getServer()); + client3.setInterface("eth0"); + + // Request an address from the subnet outside of the shared network. + ASSERT_NO_THROW(client3.requestAddress(0xabca, IOAddress("3000::1"))); + + // Request all configured options. + ASSERT_NO_THROW(client3.requestOption(D6O_NIS_SERVERS)); + ASSERT_NO_THROW(client3.requestOption(D6O_NISP_SERVERS)); + ASSERT_NO_THROW(client3.requestOption(D6O_NAME_SERVERS)); + ASSERT_NO_THROW(client3.requestOption(D6O_SNTP_SERVERS)); + + // Perform 4-way exchange and make sure we have been assigned address from the + // subnet we wanted. + testAssigned([&client3] { + ASSERT_NO_THROW(client3.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client3, IOAddress("3000::1"))); + + // This option is specified on the global level. + ASSERT_TRUE(client3.hasOptionWithAddress(D6O_NIS_SERVERS, "3000::20")); + + // Subnet specific value should be assigned. + ASSERT_TRUE(client3.hasOptionWithAddress(D6O_NISP_SERVERS, "4000::5")); +} + +// The same option is specified differently for each subnet belonging to the +// same shared network. +TEST_F(Dhcpv6SharedNetworkTest, optionsFromSelectedSubnet) { + // Create a client. + Dhcp6Client client; + client.setInterface("eth1"); + + // Create configuration with one shared network including three subnets with + // the same option having different values. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[16], *client.getServer())); + + // Client provides no hint and any subnet can be picked from the shared network. + ASSERT_NO_THROW(client.requestAddress(0xabca)); + + // Request Name Servers option. + ASSERT_NO_THROW(client.requestOption(D6O_NAME_SERVERS)); + + // Send solicit without a hint. The client should be offered an address from the + // shared network. Depending on the subnet from which the address has been allocated + // a specific value of the Name Servers option should be returned. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSolicit(true)); + }); + + if (client.hasLeaseForAddress(IOAddress("2001:db8:1::20"))) { + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "4004::22")); + + } else if (client.hasLeaseForAddress(IOAddress("2001:db8:2::20"))) { + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "5555::33")); + + } else if (client.hasLeaseForAddress(IOAddress("2001:db8:3::20"))) { + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "1234::23")); + } + + // This time let's provide a hint. + client.clearRequestedIAs(); + client.requestAddress(0xabca, IOAddress("2001:db8:2::20")); + + testAssigned([&client] { + ASSERT_NO_THROW(client.doSolicit(true)); + }); + + ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("2001:db8:2::20"))); + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "5555::33")); + + // This time, let's do the 4-way exchange. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + + ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("2001:db8:2::20"))); + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "5555::33")); + + // And renew the lease. + testAssigned([&client] { + ASSERT_NO_THROW(client.doRenew()); + }); + ASSERT_TRUE(client.hasLeaseForAddress(IOAddress("2001:db8:2::20"))); + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "5555::33")); +} + +// Different shared network is selected for different local interface. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectionByInterface) { + // Create client #1. The server receives requests from this client + // via interface eth1 and should assign shared network "frog" for + // this client. + Dhcp6Client client1; + client1.setInterface("eth1"); + client1.requestAddress(0xabca); + + // Create server configuration with two shared networks selected + // by the local interface: eth1 and eth0. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[8], *client1.getServer())); + + // Client #1 should be assigned an address from one of the two subnets + // belonging to the first shared network. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + if (!hasLeaseForAddress(client1, IOAddress("2001:db8:1::20")) && + !hasLeaseForAddress(client1, IOAddress("2001:db8:2::20"))) { + ADD_FAILURE() << "unexpected shared network selected for the client"; + } + + // Client #2. + Dhcp6Client client2; + client2.setInterface("eth0"); + client2.requestAddress(0xabca); + + // Client #2 should be assigned an address from one of the two subnets + // belonging to the second shared network. + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + if (!hasLeaseForAddress(client2, IOAddress("2001:db8:3::20")) && + !hasLeaseForAddress(client2, IOAddress("2001:db8:4::20"))) { + ADD_FAILURE() << "unexpected shared network selected for the client"; + } +} + +// Different shared network is selected for different relay address. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectionByRelay) { + // Create relayed client #1. + Dhcp6Client client1; + client1.useRelay(true, IOAddress("3000::1")); + client1.requestAddress(0xabcd); + + // Create server configuration with two shared networks selected + // by the relay address. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[9], *client1.getServer())); + + // Client #1 should be assigned an address from one of the two subnets + // belonging to the first shared network. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + if (!hasLeaseForAddress(client1, IOAddress("2001:db8:1::20")) && + !hasLeaseForAddress(client1, IOAddress("2001:db8:2::20"))) { + ADD_FAILURE() << "unexpected shared network selected for the client"; + } + + // Create relayed client #2. + Dhcp6Client client2; + client2.useRelay(true, IOAddress("3000::2")); + client2.requestAddress(0xabca); + + // Client #2 should be assigned an address from one of the two subnets + // belonging to the second shared network + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + if (!hasLeaseForAddress(client2, IOAddress("2001:db8:3::20")) && + !hasLeaseForAddress(client2, IOAddress("2001:db8:4::20"))) { + ADD_FAILURE() << "unexpected shared network selected for the client"; + } +} + +// Host reservations include hostname and client class. +TEST_F(Dhcpv6SharedNetworkTest, variousFieldsInReservation) { + // Create client #1. + Dhcp6Client client; + client.setInterface("eth1"); + client.setDUID("00:03:00:01:11:22:33:44:55:66"); + ASSERT_NO_THROW(client.requestAddress(0xabcd)); + ASSERT_NO_THROW(client.requestOption(D6O_NAME_SERVERS)); + + ASSERT_NO_THROW(client.useFQDN(Option6ClientFqdn::FLAG_S, + "bird.example.org", + Option6ClientFqdn::FULL)); + + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[10], *client.getServer())); + + // Perform 4-way exchange. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + + // The client should get an FQDN from the reservation, rather than + // the FQDN it has sent to the server. If there is a logic error, + // the server would use the first subnet from the shared network to + // assign the FQDN. This subnet has no reservation so it would + // return the same FQDN that the client has sent. We expect + // that the FQDN being sent is the one that is included in the + // reservations. + ASSERT_TRUE(client.getContext().response_); + OptionPtr opt_fqdn = client.getContext().response_->getOption(D6O_CLIENT_FQDN); + ASSERT_TRUE(opt_fqdn); + Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<Option6ClientFqdn>(opt_fqdn); + ASSERT_TRUE(fqdn); + ASSERT_EQ("test.example.org.", fqdn->getDomainName()); + + // Make sure that the correct hostname has been stored in the database. + Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, + IOAddress("2001:db8:2::20")); + ASSERT_TRUE(lease); + EXPECT_EQ("test.example.org.", lease->hostname_); + + // The DNS servers option should be derived from the client class based on the + // static class reservations. + ASSERT_TRUE(client.hasOptionWithAddress(D6O_NAME_SERVERS, "2001:db8:1::50")); +} + +// Shared network is selected based on the client class specified. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectedByClass) { + // Create client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + client1.requestAddress(0xabcd); + + // Add option 1234 which would cause the client1 to be classified as "b-devices". + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0002)); + client1.addExtraOption(option1234); + + // Configure the server with two shared networks which can be accessed + // by clients belonging to "a-devices" and "b-devices" classes + // respectively. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[11], *client1.getServer())); + + // The client 1 should be offered an address from the second subnet. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSolicit(true)); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:2::20"), + LeaseOnServer::MUST_NOT_EXIST)); + + // Create another client which will belong to a different class. + Dhcp6Client client2; + client2.setInterface("eth1"); + client2.requestAddress(0xabcd); + + /// Add option 1234 which will cause the client 2 to be classified as "a-devices". + option1234.reset(new OptionUint16(Option::V6, 1234, 0x0001)); + client2.addExtraOption(option1234); + + // Client 2 should be offered an address from the first subnet. + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSolicit(true)); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:1::20"), + LeaseOnServer::MUST_NOT_EXIST)); +} + +// Client requests two addresses and two prefixes and obtains them from two +// different subnets. +TEST_F(Dhcpv6SharedNetworkTest, assignmentsFromDifferentSubnets) { + // Create client. + Dhcp6Client client; + client.setInterface("eth1"); + client.requestAddress(0xabcd); + client.requestAddress(0x1234); + client.requestPrefix(0x1111); + client.requestPrefix(0x2222); + + // Configure the server with a shared network including two subnets. Each + // subnet has an address and prefix pool with a single available address + // and prefix respectively. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[0], *client.getServer())); + + // 4-way exchange. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + // The two addresses should come from different subnets. + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::20"))); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::20"))); + // Same for prefixes. + ASSERT_TRUE(hasLeaseForPrefixPool(client, IOAddress("4000::"), 96, 96)); + ASSERT_TRUE(hasLeaseForPrefixPool(client, IOAddress("5000::"), 96, 96)); + + // Try to renew. + testAssigned([&client] { + ASSERT_NO_THROW(client.doRenew()); + }); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::20"))); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::20"))); + ASSERT_TRUE(hasLeaseForPrefixPool(client, IOAddress("4000::"), 96, 96)); + ASSERT_TRUE(hasLeaseForPrefixPool(client, IOAddress("5000::"), 96, 96)); +} + +// Client requests 2 addresses and 2 prefixes. There is one address and one prefix +// reserved for the client. +TEST_F(Dhcpv6SharedNetworkTest, reservedAddressAndPrefix) { + // Create client. + Dhcp6Client client; + client.setInterface("eth1"); + client.setDUID("00:03:00:01:11:22:33:44:55:66"); + + // Client will request two addresses and two prefixes. + client.requestAddress(0xabcd); + client.requestAddress(0x1234); + client.requestPrefix(0x1111); + client.requestPrefix(0x2222); + + // The server configuration contains a shared network with two subnets. Each + // subnet has an address and prefix pool. One of the subnets includes a reservation + // for an address and prefix. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[4], *client.getServer())); + + // 4-way exchange. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + ASSERT_EQ(4, client.getLeaseNum()); + // The client should have got one reserved address and one reserved prefix. + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::28"))); + ASSERT_TRUE(hasLeaseForPrefix(client, IOAddress("5000::8:0000"), 112, IAID(0x1111))); + + // The client should have got dynamically allocated address too and it must be + // different than the reserved address. + std::vector<Lease6> leases_1234 = client.getLeasesByIAID(0x1234); + ASSERT_EQ(1, leases_1234.size()); + ASSERT_NE("2001:db8:2::28", leases_1234[0].addr_.toText()); + + // Same for prefix. + std::vector<Lease6> leases_2222 = client.getLeasesByIAID(0x2222); + ASSERT_EQ(1, leases_2222.size()); + ASSERT_NE("1234::", leases_2222[0].addr_.toText()); + + // Try to renew and check this again. + testAssigned([&client] { + ASSERT_NO_THROW(client.doRenew()); + }); + ASSERT_EQ(4, client.getLeaseNum()); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::28"))); + ASSERT_TRUE(hasLeaseForPrefix(client, IOAddress("5000::8:0000"), 112, IAID(0x1111))); + + leases_1234 = client.getLeasesByIAID(0x1234); + ASSERT_EQ(1, leases_1234.size()); + ASSERT_NE("2001:db8:2::28", leases_1234[0].addr_.toText()); + + leases_2222 = client.getLeasesByIAID(0x2222); + ASSERT_EQ(1, leases_2222.size()); + ASSERT_NE(IOAddress("5000::8:0000").toText(), leases_2222[0].addr_.toText()); +} + +// Relay address is specified for each subnet within shared network. +TEST_F(Dhcpv6SharedNetworkTest, relaySpecifiedForEachSubnet) { + // Create client. + Dhcp6Client client; + client.useRelay(true, IOAddress("3001::1")); + + // Client will request two addresses. + client.requestAddress(0xabcd); + client.requestAddress(0x1234); + + // Configure the server with three subnets. Two of them belong to a shared network. + // Each subnet is configured with relay info, i.e. IP address of the relay agent + // for which the shared network is used. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[13], *client.getServer())); + + // 4-way exchange. + testAssigned([&client] { + ASSERT_NO_THROW(client.doSARR()); + }); + ASSERT_EQ(2, client.getLeaseNum()); + + // The client should have got two leases, one from each subnet within the + // shared network. + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:1::20"))); + ASSERT_TRUE(hasLeaseForAddress(client, IOAddress("2001:db8:2::20"))); +} + +// Shared network is selected based on interface id. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectedByInterfaceId) { + // Create client #1. This is a relayed client for which interface id + // has been specified and this interface id is matching the one specified + // for the shared network. + Dhcp6Client client1; + client1.useRelay(true, IOAddress("3001::1")); + client1.useInterfaceId("vlan10"); + + // Configure the server with one shared network and one subnet outside of the + // shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[14], *client1.getServer())); + + // Client #1 should be assigned an address from shared network. + ASSERT_NO_THROW(client1.requestAddress(0xabca0)); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Create client #2. This is a relayed client which is using interface id + // matching a subnet outside of the shared network. + Dhcp6Client client2(client1.getServer()); + client2.useRelay(true, IOAddress("3001::2")); + client2.useInterfaceId("vlan1000"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); +} + +// Shared network is selected based on interface id specified for a subnet +// belonging to a shared network. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkSelectedByInterfaceIdInSubnet) { + // Create client #1. This is a relayed client for which interface id + // has been specified and this interface id is matching the one specified + // for the shared network. + Dhcp6Client client1; + client1.useRelay(true, IOAddress("3001::1")); + client1.useInterfaceId("vlan10"); + + // Configure the server with one shared network and one subnet outside of the + // shared network. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[15], *client1.getServer())); + + // Client #1 should be assigned an address from shared network. + ASSERT_NO_THROW(client1.requestAddress(0xabca0)); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Create client #2. This is a relayed client which is using interface id + // matching a subnet outside of the shared network. + Dhcp6Client client2(client1.getServer()); + client2.useRelay(true, IOAddress("3001::2")); + client2.useInterfaceId("vlan1000"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:2::20"))); +} + +// Check that the rapid-commit works with shared networks. Rapid-commit +// enabled on each subnet separately. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkRapidCommit1) { + testRapidCommit(NETWORKS_CONFIG[17], true, "2001:db8:1::20", "2001:db8:2::20"); +} + +// Check that the rapid-commit works with shared networks. Rapid-commit +// enabled for the whole shared network. This should be applied to both +// subnets. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkRapidCommit2) { + testRapidCommit(NETWORKS_CONFIG[18], true, "2001:db8:1::20", "2001:db8:2::20"); +} + +// Check that the rapid-commit is disabled by default. +TEST_F(Dhcpv6SharedNetworkTest, sharedNetworkRapidCommit3) { + testRapidCommit(NETWORKS_CONFIG[1], false, "", ""); +} + +// Pool is selected based on the client class specified. +TEST_F(Dhcpv6SharedNetworkTest, poolInSharedNetworkSelectedByClass) { + // Create client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + + // Configure the server with one shared network including one subnet and + // two pools. The access to one of the pools is restricted by + // by client classification. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[19], *client1.getServer())); + + // Client #1 requests an address in the restricted pool but can't be assigned + // this address because the client doesn't belong to a certain class. + ASSERT_NO_THROW(client1.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::50"))); + + // Release the lease that the client has got, because we'll need this address + // further in the test. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRelease()); + }); + + // Add option 1234 which would cause the client to be classified as "a-devices". + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0001)); + client1.addExtraOption(option1234); + + // This time, the allocation of the address provided as hint should be successful. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Client 2 should be assigned an address from the unrestricted pool. + Dhcp6Client client2(client1.getServer()); + client2.setInterface("eth1"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:1::50"))); + + // Now, let's reconfigure the server to also apply restrictions on the + // pool to which client2 now belongs. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[20], *client1.getServer())); + + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(0, client2.getLeasesWithNonZeroLifetime().size()); + + // If we add option 1234 with a value matching this class, the lease should + // get renewed. + OptionPtr option1234_bis(new OptionUint16(Option::V6, 1234, 0x0002)); + client2.addExtraOption(option1234_bis); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(1, client2.getLeaseNum()); + EXPECT_EQ(1, client2.getLeasesWithNonZeroLifetime().size()); +} + +// Pool is selected based on the client class specified using a plain subnet. +TEST_F(Dhcpv6SharedNetworkTest, poolInSubnetSelectedByClass) { + // Create client #1. + Dhcp6Client client1; + client1.setInterface("eth1"); + + // Configure the server with one plain subnet including two pools. + // The access to one of the pools is restricted by client classification. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[21], *client1.getServer())); + + // Client #1 requests an address in the restricted pool but can't be assigned + // this address because the client doesn't belong to a certain class. + ASSERT_NO_THROW(client1.requestAddress(0xabca, IOAddress("2001:db8:1::20"))); + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::50"))); + + // Release the lease that the client has got, because we'll need this address + // further in the test. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doRelease()); + }); + + // Add option 1234 which would cause the client to be classified as "a-devices". + OptionPtr option1234(new OptionUint16(Option::V6, 1234, 0x0001)); + client1.addExtraOption(option1234); + + // This time, the allocation of the address provided as hint should be successful. + testAssigned([&client1] { + ASSERT_NO_THROW(client1.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client1, IOAddress("2001:db8:1::20"))); + + // Client 2 should be assigned an address from the unrestricted pool. + Dhcp6Client client2(client1.getServer()); + client2.setInterface("eth1"); + ASSERT_NO_THROW(client2.requestAddress(0xabca0)); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doSARR()); + }); + ASSERT_TRUE(hasLeaseForAddress(client2, IOAddress("2001:db8:1::50"))); + + // Now, let's reconfigure the server to also apply restrictions on the + // pool to which client2 now belongs. + ASSERT_NO_FATAL_FAILURE(configure(NETWORKS_CONFIG[22], *client1.getServer())); + + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(0, client2.getLeasesWithNonZeroLifetime().size()); + + // If we add option 1234 with a value matching this class, the lease should + // get renewed. + OptionPtr option1234_bis(new OptionUint16(Option::V6, 1234, 0x0002)); + client2.addExtraOption(option1234_bis); + testAssigned([&client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(1, client2.getLeaseNum()); + EXPECT_EQ(1, client2.getLeasesWithNonZeroLifetime().size()); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceGlobal) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::1"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceClass) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::2"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceClasses) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"beta\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }," + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::3\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + // Class order is the insert order + testPrecedence(config, "2001:db8:1::2"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceNetworkClass) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::3\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::3"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceSubnet) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::3\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::4\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::4"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedencePool) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::3\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::4\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::5\"" + " }" + " ]" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::5"); +} + +// Verify option processing precedence +// Order is global < class < shared-network < subnet < pools < host reservation +TEST_F(Dhcpv6SharedNetworkTest, precedenceReservation) { + const std::string config = + "{" + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::1\"" + " }" + " ]," + " \"client-classes\": [" + " {" + " \"name\": \"alpha\"," + " \"test\": \"'' == ''\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::2\"" + " }" + " ]" + " }" + " ]," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::3\"" + " }" + " ]," + " \"subnet6\": [" + " {" + " \"subnet\": \"2001:db8:1::/64\"," + " \"id\": 10," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::4\"" + " }" + " ]," + " \"pools\": [" + " {" + " \"pool\": \"2001:db8:1::1 - 2001:db8:1::64\"," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::5\"" + " }" + " ]" + " }" + " ]," + " \"reservations\": [" + " {" + " \"duid\": \"00:03:00:01:aa:bb:cc:dd:ee:ff\"," + " \"ip-addresses\": [ \"2001:db8:1::28\" ]," + " \"option-data\": [" + " {" + " \"name\": \"dns-servers\"," + " \"data\": \"2001:db8:1::6\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}"; + + testPrecedence(config, "2001:db8:1::6"); +} + +} // end of anonymous namespace |