From 300cb6367058af3fe36731f08f8b7eea7b12d03c Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Fri, 19 Jan 2018 00:13:48 +0100 Subject: [PATCH] [5425a] Applied changes - need regen --- doc/examples/kea4/classify.json | 22 +- doc/examples/kea6/classify.json | 12 + doc/guide/classify.xml | 50 ++++ doc/guide/dhcp4-srv.xml | 9 + doc/guide/dhcp6-srv.xml | 10 + src/bin/dhcp4/dhcp4_lexer.ll | 1 + src/bin/dhcp4/dhcp4_parser.yy | 7 +- src/bin/dhcp4/tests/config_parser_unittest.cc | 90 ++++++ src/bin/dhcp4/tests/dhcp4_srv_unittest.cc | 69 +++++ .../dhcp4/tests/shared_network_unittest.cc | 273 +++++++++++++++++- src/bin/dhcp6/dhcp6_lexer.ll | 32 +- src/bin/dhcp6/dhcp6_messages.mes | 2 +- src/bin/dhcp6/dhcp6_parser.yy | 8 +- src/bin/dhcp6/tests/classify_unittests.cc | 92 ++++++ src/bin/dhcp6/tests/config_parser_unittest.cc | 188 ++++++++++++ .../dhcp6/tests/shared_network_unittest.cc | 271 +++++++++++++++++ 16 files changed, 1112 insertions(+), 24 deletions(-) diff --git a/doc/examples/kea4/classify.json b/doc/examples/kea4/classify.json index 6964960e22..e769e581f0 100644 --- a/doc/examples/kea4/classify.json +++ b/doc/examples/kea4/classify.json @@ -97,7 +97,27 @@ "client-classes": [ "VoIP" ] } ], "interface": "ethX" - } + }, + +// The following list defines a subnet with pools. For some pools +// we defined a class that is allowed in that pool. If not specified +// everyone is allowed. When a class is specified, only packets belonging +// to that class are allowed for that pool. + { + "pools": [ + { +// This one is for VoIP devices only. + "pool": "192.0.4.1 - 192.0.4.200", + "client-class": "VoIP" + }, +// This one doesn't have any client-class specified, so everyone +// is allowed in. + { + "pool": "192.0.5.1 - 192.0.5.200" + } ], + "subnet": "192.0.4.0/23", + "interface": "ethY" + } ] }, diff --git a/doc/examples/kea6/classify.json b/doc/examples/kea6/classify.json index eaa37beec4..b6e184b618 100644 --- a/doc/examples/kea6/classify.json +++ b/doc/examples/kea6/classify.json @@ -73,7 +73,19 @@ "client-classes": [ "cable-modems" ] } ], "interface": "ethX" + }, +// The following subnet contains a pool with a class constraint: only +// clients which belong to the class are allowed to use this pool. + { + "pools": [ + { + "pool": "2001:db8:3::/80", + "client-class": "cable-modems" + } ], + "subnet": "2001:db8:4::/64", + "interface": "ethY" } + ] }, diff --git a/doc/guide/classify.xml b/doc/guide/classify.xml index cfc515f9a1..bd5ab03907 100644 --- a/doc/guide/classify.xml +++ b/doc/guide/classify.xml @@ -801,6 +801,56 @@ concatenation of the strings +
+ Configuring Pools With Class Information + + Similar to subnets in certain cases access to certain address or + prefix pools must be restricted to only clients that belong to a + given class, using the "client-class" when defining the pool. + + + + Let's assume that the server is connected to a network segment that uses + the 192.0.2.0/24 prefix. The Administrator of that network has decided + that addresses from range 192.0.2.10 to 192.0.2.20 are going to be + managed by the DHCP4 server. Only clients belonging to client class + Client_foo are allowed to use this pool. Such a + configuration can be achieved in the following way: + +"Dhcp4": { + "client-classes": [ + { + "name": "Client_foo", + "test": "substring(option[61].hex,0,3) == 'foo'", + "option-data": [ + { + "name": "domain-name-servers", + "code": 6, + "space": "dhcp4", + "csv-format": true, + "data": "192.0.2.1, 192.0.2.2" + } + ] + }, + ... + ], + "subnet4": [ + { + "subnet": "192.0.2.0/24", + "pools": [ + { + "pool": "192.0.2.10 - 192.0.2.20", + "client-class": "Client_foo" + } + ] + }, + ... + ],, + ... +} + +
+
Using Classes diff --git a/doc/guide/dhcp4-srv.xml b/doc/guide/dhcp4-srv.xml index 1f7a1eaf37..c83ce6333d 100644 --- a/doc/guide/dhcp4-srv.xml +++ b/doc/guide/dhcp4-srv.xml @@ -2092,6 +2092,15 @@ It is merely echoed by the server class restrictions on subnets, see . + + When subnets belong to a shared network the classification applies + to subnet selection but not to pools, e.g., a pool in a subnet + limited to a particular class can still be used by clients which do not + belong to the class if the pool they are expected to use is exhausted. + So the limit access based on class information is also available + at the pool level, see . + + The process of doing classification is conducted in three steps. The first step is to assess an incoming packet and assign it to zero or more classes. The diff --git a/doc/guide/dhcp6-srv.xml b/doc/guide/dhcp6-srv.xml index f0b96148c7..74ec99a5c1 100644 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@ -1950,6 +1950,16 @@ should include options from the isc option space: class restrictions on subnets, see . + + When subnets belong to a shared network the classification applies + to subnet selection but not to pools, e.g., a pool in a subnet + limited to a particular class can still be used by clients which do not + belong to the class if the pool they are expected to use is exhausted. + So the limit access based on class information is also available + at the address/prefix pool level, see . + + The process of doing classification is conducted in three steps. The first step is to assess an incoming packet and assign it to zero or more classes. The diff --git a/src/bin/dhcp4/dhcp4_lexer.ll b/src/bin/dhcp4/dhcp4_lexer.ll index d2b4b59153..d5d88ff41a 100644 --- a/src/bin/dhcp4/dhcp4_lexer.ll +++ b/src/bin/dhcp4/dhcp4_lexer.ll @@ -823,6 +823,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"client-class\" { switch(driver.ctx_) { case isc::dhcp::Parser4Context::SUBNET4: + case isc::dhcp::Parser4Context::POOLS: case isc::dhcp::Parser4Context::SHARED_NETWORK: case isc::dhcp::Parser4Context::CLIENT_CLASSES: return isc::dhcp::Dhcp4Parser::make_CLIENT_CLASS(driver.loc_); diff --git a/src/bin/dhcp4/dhcp4_parser.yy b/src/bin/dhcp4/dhcp4_parser.yy index 6d4c574992..9c5f94262a 100644 --- a/src/bin/dhcp4/dhcp4_parser.yy +++ b/src/bin/dhcp4/dhcp4_parser.yy @@ -1344,6 +1344,7 @@ pool_params: pool_param pool_param: pool_entry | option_data_list + | client_class | user_context | comment | unknown_map_entry @@ -1598,11 +1599,11 @@ client_classes: CLIENT_CLASSES { ctx.leave(); }; -client_classes_list: client_class - | client_classes_list COMMA client_class +client_classes_list: client_class_entry + | client_classes_list COMMA client_class_entry ; -client_class: LCURLY_BRACKET { +client_class_entry: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.back()->add(m); ctx.stack_.push_back(m); diff --git a/src/bin/dhcp4/tests/config_parser_unittest.cc b/src/bin/dhcp4/tests/config_parser_unittest.cc index 0653b5c7e1..da4b250be7 100644 --- a/src/bin/dhcp4/tests/config_parser_unittest.cc +++ b/src/bin/dhcp4/tests/config_parser_unittest.cc @@ -4100,6 +4100,7 @@ TEST_F(Dhcp4ParserTest, classifySubnets) { ConstElementPtr json; ASSERT_NO_THROW(json = parseDHCP4(config)); + extractConfig(config); EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json)); checkResult(x, 0); @@ -4157,6 +4158,95 @@ TEST_F(Dhcp4ParserTest, classifySubnets) { EXPECT_TRUE (subnets->at(3)->clientSupported(classes)); } +// Goal of this test is to verify that multiple pools can be configured +// with defined client classes. +TEST_F(Dhcp4ParserTest, classifyPools) { + ConstElementPtr x; + string config = "{ " + genIfaceConfig() + "," + + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet4\": [ { " + " \"pools\": [ { " + " \"pool\": \"192.0.2.1 - 192.0.2.100\", " + " \"client-class\": \"alpha\" " + " }," + " {" + " \"pool\": \"192.0.3.101 - 192.0.3.150\", " + " \"client-class\": \"beta\" " + " }," + " {" + " \"pool\": \"192.0.4.101 - 192.0.4.150\", " + " \"client-class\": \"gamma\" " + " }," + " {" + " \"pool\": \"192.0.5.101 - 192.0.5.150\" " + " } ]," + " \"subnet\": \"192.0.0.0/16\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config, true)); + extractConfig(config); + + EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json)); + checkResult(x, 0); + + const Subnet4Collection* subnets = + CfgMgr::instance().getStagingCfg()->getCfgSubnets4()->getAll(); + ASSERT_TRUE(subnets); + ASSERT_EQ(1, subnets->size()); + const PoolCollection& pools = subnets->at(0)->getPools(Lease::TYPE_V4); + ASSERT_EQ(4, pools.size()); // We expect 4 pools + + // Let's check if client belonging to alpha class is supported in pool[0] + // and not supported in any other pool (except pool[3], which allows + // everyone). + ClientClasses classes; + classes.insert("alpha"); + EXPECT_TRUE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE(pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to beta class is supported in pool[1] + // and not supported in any other pool (except pools[3], which allows + // everyone). + classes.clear(); + classes.insert("beta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_TRUE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE(pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to gamma class is supported in pool[2] + // and not supported in any other pool (except pool[3], which allows + // everyone). + classes.clear(); + classes.insert("gamma"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_TRUE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE(pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to some other class (not mentioned in + // the config) is supported only in pool[3], which allows everyone. + classes.clear(); + classes.insert("delta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE(pools.at(3)->clientSupported(classes)); + + // Finally, let's check class-less client. He should be allowed only in + // the last pool, which does not have any class restrictions. + classes.clear(); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE(pools.at(3)->clientSupported(classes)); +} + // This test verifies that the host reservations can be specified for // respective IPv4 subnets. TEST_F(Dhcp4ParserTest, reservations) { diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc index df1ae36fb9..f650d33867 100644 --- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc @@ -2318,6 +2318,75 @@ TEST_F(Dhcpv4SrvTest, clientClassify) { EXPECT_TRUE(srv_.selectSubnet(dis)); } +// Checks if the client-class field is indeed used for pool selection. +TEST_F(Dhcpv4SrvTest, clientPoolClassify) { + IfaceMgrTestConfig test_config(true); + IfaceMgr::instance().openSockets4(); + + NakedDhcpv4Srv srv(0); + + // This test configures 2 pools. + // The second pool does not play any role here. The client's + // IP address belongs to the first pool, so only that first + // pool is being tested. + string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet4\": [ " + "{ \"pools\": [ { " + " \"pool\": \"192.0.2.1 - 192.0.2.100\", " + " \"client-class\": \"foo\" }, " + " { \"pool\": \"192.0.3.1 - 192.0.3.100\", " + " \"client-class\": \"xyzzy\" } ], " + " \"subnet\": \"192.0.0.0/16\" } " + "]," + "\"valid-lifetime\": 4000 }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP4(config, true)); + + ConstElementPtr status; + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + + CfgMgr::instance().commit(); + + // check if returned status is OK + ASSERT_TRUE(status); + comment_ = config::parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + // Create a simple packet that we'll use for classification + Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234)); + dis->setRemoteAddr(IOAddress("192.0.2.1")); + dis->setCiaddr(IOAddress("192.0.2.1")); + dis->setIface("eth0"); + OptionPtr clientid = generateClientId(); + dis->addOption(clientid); + + // This discover does not belong to foo class, so it will not + // be serviced + Pkt4Ptr offer = srv.processDiscover(dis); + EXPECT_FALSE(offer); + + // Let's add the packet to bar class and try again. + dis->addClass("bar"); + + // Still not supported, because it belongs to wrong class. + offer = srv.processDiscover(dis); + EXPECT_FALSE(offer); + + // Let's add it to matching class. + dis->addClass("foo"); + + // This time it should work + offer = srv.processDiscover(dis); + ASSERT_TRUE(offer); + EXPECT_EQ(DHCPOFFER, offer->getType()); + EXPECT_FALSE(offer->getYiaddr().isV4Zero()); +} + // Verifies last resort option 43 is backward compatible TEST_F(Dhcpv4SrvTest, option43LastResort) { IfaceMgrTestConfig test_config(true); diff --git a/src/bin/dhcp4/tests/shared_network_unittest.cc b/src/bin/dhcp4/tests/shared_network_unittest.cc index c5c47007da..64240ec6f5 100644 --- a/src/bin/dhcp4/tests/shared_network_unittest.cc +++ b/src/bin/dhcp4/tests/shared_network_unittest.cc @@ -700,7 +700,7 @@ const char* NETWORKS_CONFIG[] = { // Configuration #13. // - 2 classes -// - 2 shared networks, each with 1 subnet and client class restricton +// - 2 shared networks, each with 1 subnet and client class restriction "{" " \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -861,6 +861,158 @@ const char* NETWORKS_CONFIG[] = { " ]" " }" " ]" + "}", + +// Configuration #16 +// - 1 shared network with 1 subnet and 2 pools (first pool has class restriction) + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[93].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[93].hex == 0x0002\"" + " }" + " ]," + " \"valid-lifetime\": 600," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet4\": [" + " {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"192.0.2.1 - 192.0.2.63\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"192.0.2.100 - 192.0.2.100\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #17 +// - 1 shared network with 1 subnet and 2 pools (each with class restriction) + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[93].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[93].hex == 0x0002\"" + " }" + " ]," + " \"valid-lifetime\": 600," + " \"shared-networks\": [" + " {" + " \"name\": \"frog\"," + " \"interface\": \"eth1\"," + " \"subnet4\": [" + " {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 10," + " \"pools\": [" + " {" + " \"pool\": \"192.0.2.1 - 192.0.2.63\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"192.0.2.100 - 192.0.2.100\"," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #18 +// - plain subnet and 2 pools (first pool has class restriction) + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[93].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[93].hex == 0x0002\"" + " }" + " ]," + " \"valid-lifetime\": 600," + " \"subnet4\": [" + " {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 10," + " \"interface\": \"eth1\"," + " \"pools\": [" + " {" + " \"pool\": \"192.0.2.1 - 192.0.2.63\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"192.0.2.100 - 192.0.2.100\"" + " }" + " ]" + " }" + " ]" + "}", + +// Configuration #19 +// - plain subnet and 2 pools (each with class restriction) + "{" + " \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + " }," + " \"client-classes\": [" + " {" + " \"name\": \"a-devices\"," + " \"test\": \"option[93].hex == 0x0001\"" + " }," + " {" + " \"name\": \"b-devices\"," + " \"test\": \"option[93].hex == 0x0002\"" + " }" + " ]," + " \"valid-lifetime\": 600," + " \"subnet4\": [" + " {" + " \"subnet\": \"192.0.2.0/24\"," + " \"id\": 10," + " \"interface\": \"eth1\"," + " \"pools\": [" + " {" + " \"pool\": \"192.0.2.1 - 192.0.2.63\"," + " \"client-class\": \"a-devices\"" + " }," + " {" + " \"pool\": \"192.0.2.100 - 192.0.2.100\"," + " \"client-class\": \"b-devices\"" + " }" + " ]" + " }" + " ]" "}" }; @@ -1813,4 +1965,123 @@ TEST_F(Dhcpv4SharedNetworkTest, customServerIdentifier) { EXPECT_EQ("2.3.4.5", client2.config_.serverid_.toText()); } +// Access to a pool within shared network is restricted by client +// classification. +TEST_F(Dhcpv4SharedNetworkTest, poolInSharedNetworkSelectedByClass) { + // Create client #1 + Dhcp4Client client1(Dhcp4Client::SELECTING); + client1.setIfaceName("eth1"); + + // Configure the server with one shared network including one subnet and + // in 2 pools in it. The access to one of the pools is restricted + // by client classification. + configure(NETWORKS_CONFIG[16], *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. + testAssigned([this, &client1] { + doDORA(client1, "192.0.2.100", "192.0.2.63"); + }); + + // Release the lease that the client has got, because we'll need this address + // further in the test. + testAssigned([this, &client1] { + ASSERT_NO_THROW(client1.doRelease()); + }); + + // Add option93 which would cause the client to be classified as "a-devices". + OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001)); + client1.addExtraOption(option93); + + // This time, the allocation of the address provided as hint should be successful. + testAssigned([this, &client1] { + doDORA(client1, "192.0.2.63", "192.0.2.63"); + }); + + // Client 2 should be assigned an address from the unrestricted pool. + Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING); + client2.setIfaceName("eth1"); + testAssigned([this, &client2] { + doDORA(client2, "192.0.2.100"); + }); + + // Now, let's reconfigure the server to also apply restrictions on the + // pool to which client2 now belongs. + configure(NETWORKS_CONFIG[17], *client1.getServer()); + + // The client should be refused to renew the lease because it doesn't belong + // to "b-devices" class. + client2.setState(Dhcp4Client::RENEWING); + testAssigned([this, &client2] { + doRequest(client2, ""); + }); + + // If we add option93 with a value matching this class, the lease should + // get renewed. + OptionPtr option93_bis(new OptionUint16(Option::V4, 93, 0x0002)); + client2.addExtraOption(option93_bis); + + testAssigned([this, &client2] { + doRequest(client2, "192.0.2.100"); + }); +} + +// Access to a pool within plain subnet is restricted by client classification. +TEST_F(Dhcpv4SharedNetworkTest, poolInSubnetSelectedByClass) { + // Create client #1 + Dhcp4Client client1(Dhcp4Client::SELECTING); + client1.setIfaceName("eth1"); + + // Configure the server with one plain subnet including two pools. + // The access to one of the pools is restricted by client classification. + configure(NETWORKS_CONFIG[18], *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. + testAssigned([this, &client1] { + doDORA(client1, "192.0.2.100", "192.0.2.63"); + }); + + // Release the lease that the client has got, because we'll need this address + // further in the test. + testAssigned([this, &client1] { + ASSERT_NO_THROW(client1.doRelease()); + }); + + // Add option93 which would cause the client to be classified as "a-devices". + OptionPtr option93(new OptionUint16(Option::V4, 93, 0x0001)); + client1.addExtraOption(option93); + + // This time, the allocation of the address provided as hint should be successful. + testAssigned([this, &client1] { + doDORA(client1, "192.0.2.63", "192.0.2.63"); + }); + + // Client 2 should be assigned an address from the unrestricted pool. + Dhcp4Client client2(client1.getServer(), Dhcp4Client::SELECTING); + client2.setIfaceName("eth1"); + testAssigned([this, &client2] { + doDORA(client2, "192.0.2.100"); + }); + + // Now, let's reconfigure the server to also apply restrictions on the + // pool to which client2 now belongs. + configure(NETWORKS_CONFIG[19], *client1.getServer()); + + // The client should be refused to renew the lease because it doesn't belong + // to "b-devices" class. + client2.setState(Dhcp4Client::RENEWING); + testAssigned([this, &client2] { + doRequest(client2, ""); + }); + + // If we add option93 with a value matching this class, the lease should + // get renewed. + OptionPtr option93_bis(new OptionUint16(Option::V4, 93, 0x0002)); + client2.addExtraOption(option93_bis); + + testAssigned([this, &client2] { + doRequest(client2, "192.0.2.100"); + }); +} } // end of anonymous namespace diff --git a/src/bin/dhcp6/dhcp6_lexer.ll b/src/bin/dhcp6/dhcp6_lexer.ll index 4805a86bfc..58808613ea 100644 --- a/src/bin/dhcp6/dhcp6_lexer.ll +++ b/src/bin/dhcp6/dhcp6_lexer.ll @@ -609,7 +609,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} switch(driver.ctx_) { case isc::dhcp::Parser6Context::DHCP6: case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_PREFERRED_LIFETIME(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("preferred-lifetime", driver.loc_); @@ -620,7 +620,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} switch(driver.ctx_) { case isc::dhcp::Parser6Context::DHCP6: case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_VALID_LIFETIME(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("valid-lifetime", driver.loc_); @@ -631,7 +631,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} switch(driver.ctx_) { case isc::dhcp::Parser6Context::DHCP6: case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_RENEW_TIMER(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("renew-timer", driver.loc_); @@ -642,7 +642,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} switch(driver.ctx_) { case isc::dhcp::Parser6Context::DHCP6: case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_REBIND_TIMER(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("rebind-timer", driver.loc_); @@ -661,7 +661,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"subnet6\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::DHCP6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_SUBNET6(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("subnet6", driver.loc_); @@ -670,7 +670,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"shared-networks\" { switch (driver.ctx_) { - case Parser6Context::DHCP6: + case isc::dhcp::Parser6Context::DHCP6: return Dhcp6Parser::make_SHARED_NETWORKS(driver.loc_); default: return Dhcp6Parser::make_STRING("shared-networks", driver.loc_); @@ -695,7 +695,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} case isc::dhcp::Parser6Context::RESERVATIONS: case isc::dhcp::Parser6Context::CLIENT_CLASSES: case isc::dhcp::Parser6Context::CLIENT_CLASS: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_OPTION_DATA(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("option-data", driver.loc_); @@ -711,7 +711,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} case isc::dhcp::Parser6Context::CLIENT_CLASSES: case isc::dhcp::Parser6Context::CLIENT_CLASS: case isc::dhcp::Parser6Context::LOGGERS: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_NAME(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("name", driver.loc_); @@ -864,7 +864,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"interface\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_INTERFACE(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("interface", driver.loc_); @@ -874,7 +874,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"interface-id\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_INTERFACE_ID(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("interface-id", driver.loc_); @@ -893,7 +893,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"rapid-commit\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_RAPID_COMMIT(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("rapid-commit", driver.loc_); @@ -903,7 +903,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"reservation-mode\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_RESERVATION_MODE(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("reservation-mode", driver.loc_); @@ -1078,8 +1078,10 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"client-class\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: + case isc::dhcp::Parser6Context::POOLS: + case isc::dhcp::Parser6Context::PD_POOLS: case isc::dhcp::Parser6Context::CLIENT_CLASSES: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_CLIENT_CLASS(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("client-class", driver.loc_); @@ -1212,7 +1214,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"relay\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::SUBNET6: - case Parser6Context::SHARED_NETWORK: + case isc::dhcp::Parser6Context::SHARED_NETWORK: return isc::dhcp::Dhcp6Parser::make_RELAY(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("relay", driver.loc_); @@ -1222,7 +1224,7 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} \"ip-address\" { switch(driver.ctx_) { case isc::dhcp::Parser6Context::RELAY: - return isc::dhcp::Dhcp6Parser::make_IP_ADDRESS(driver.loc_); + return isc::dhcp::Dhcp6Parser::make_IP_ADDRESS(driver.loc_); default: return isc::dhcp::Dhcp6Parser::make_STRING("ip-address", driver.loc_); } diff --git a/src/bin/dhcp6/dhcp6_messages.mes b/src/bin/dhcp6/dhcp6_messages.mes index 0d6c9caebe..1653dec66d 100644 --- a/src/bin/dhcp6/dhcp6_messages.mes +++ b/src/bin/dhcp6/dhcp6_messages.mes @@ -60,7 +60,7 @@ The first argument specifies the client and transaction identification information. The second argument includes all classes to which the packet has been assigned. -% DHCP6_CLASS_UNCONFIGURED %1: client packet belongs to an unconfigured class: %1 +% DHCP6_CLASS_UNCONFIGURED %1: client packet belongs to an unconfigured class: %2 This debug message informs that incoming packet belongs to a class which cannot be found in the configuration. Either a hook written before the classification was added to Kea is used, or class naming is diff --git a/src/bin/dhcp6/dhcp6_parser.yy b/src/bin/dhcp6/dhcp6_parser.yy index 15c0e8ebb5..5fd8df6db6 100644 --- a/src/bin/dhcp6/dhcp6_parser.yy +++ b/src/bin/dhcp6/dhcp6_parser.yy @@ -1307,6 +1307,7 @@ pool_params: pool_param pool_param: pool_entry | option_data_list + | client_class | user_context | comment | unknown_map_entry @@ -1427,6 +1428,7 @@ pd_pool_param: pd_prefix | pd_prefix_len | pd_delegated_len | option_data_list + | client_class | excluded_prefix | excluded_prefix_len | user_context @@ -1622,11 +1624,11 @@ client_classes: CLIENT_CLASSES { ctx.leave(); }; -client_classes_list: client_class - | client_classes_list COMMA client_class +client_classes_list: client_class_entry + | client_classes_list COMMA client_class_entry ; -client_class: LCURLY_BRACKET { +client_class_entry: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.back()->add(m); ctx.stack_.push_back(m); diff --git a/src/bin/dhcp6/tests/classify_unittests.cc b/src/bin/dhcp6/tests/classify_unittests.cc index 9b04d035c2..7a81310f92 100644 --- a/src/bin/dhcp6/tests/classify_unittests.cc +++ b/src/bin/dhcp6/tests/classify_unittests.cc @@ -643,6 +643,98 @@ TEST_F(ClassifyTest, clientClassifySubnet) { EXPECT_TRUE(srv_.selectSubnet(sol)); } +// Checks if the client-class field is indeed used for pool selection. +TEST_F(ClassifyTest, clientClassifyPool) { + IfaceMgrTestConfig test_config(true); + + NakedDhcpv6Srv srv(0); + + // This test configures 2 pools. + // The second pool does not play any role here. The client's + // IP address belongs to the first pool, so only that first + // pool is being tested. + std::string config = "{ \"interfaces-config\": {" + " \"interfaces\": [ \"*\" ]" + "}," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"client-classes\": [ " + " { " + " \"name\": \"foo\" " + " }, " + " { " + " \"name\": \"bar\" " + " } " + "], " + "\"subnet6\": [ " + " { \"pools\": [ " + " { " + " \"pool\": \"2001:db8:1::/64\", " + " \"client-class\": \"foo\" " + " }, " + " { " + " \"pool\": \"2001:db8:2::/64\", " + " \"client-class\": \"xyzzy\" " + " } " + " ], " + " \"subnet\": \"2001:db8:2::/40\" " + " } " + "], " + "\"valid-lifetime\": 4000 }"; + + ASSERT_NO_THROW(configure(config)); + + OptionPtr clientid = generateClientId(); + Pkt6Ptr query1 = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + query1->setRemoteAddr(IOAddress("2001:db8:1::3")); + query1->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + query1->addOption(clientid); + query1->setIface("eth1"); + Pkt6Ptr query2 = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + query2->setRemoteAddr(IOAddress("2001:db8:1::3")); + query2->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + query2->addOption(clientid); + query2->setIface("eth1"); + Pkt6Ptr query3 = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + query3->setRemoteAddr(IOAddress("2001:db8:1::3")); + query3->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + query3->addOption(clientid); + query3->setIface("eth1"); + + // This discover does not belong to foo class, so it will not + // be serviced + srv.classifyPacket(query1); + Pkt6Ptr response1 = srv.processSolicit(query1); + ASSERT_TRUE(response1); + OptionPtr ia_na1 = response1->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na1); + EXPECT_TRUE(ia_na1->getOption(D6O_STATUS_CODE)); + EXPECT_FALSE(ia_na1->getOption(D6O_IAADDR)); + + // Let's add the packet to bar class and try again. + query2->addClass("bar"); + // Still not supported, because it belongs to wrong class. + srv.classifyPacket(query2); + Pkt6Ptr response2 = srv.processSolicit(query2); + ASSERT_TRUE(response2); + OptionPtr ia_na2 = response2->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na2); + EXPECT_TRUE(ia_na2->getOption(D6O_STATUS_CODE)); + EXPECT_FALSE(ia_na2->getOption(D6O_IAADDR)); + + // Let's add it to matching class. + query3->addClass("foo"); + // This time it should work + srv.classifyPacket(query3); + Pkt6Ptr response3 = srv.processSolicit(query3); + ASSERT_TRUE(response3); + OptionPtr ia_na3 = response3->getOption(D6O_IA_NA); + ASSERT_TRUE(ia_na3); + EXPECT_FALSE(ia_na3->getOption(D6O_STATUS_CODE)); + EXPECT_TRUE(ia_na3->getOption(D6O_IAADDR)); +} + // Tests whether a packet with custom vendor-class (not erouter or docsis) // is classified properly. TEST_F(ClassifyTest, vendorClientClassification2) { diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index fd18d54435..ead3a0d958 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -4211,6 +4211,194 @@ TEST_F(Dhcp6ParserTest, classifySubnets) { EXPECT_TRUE (subnets->at(3)->clientSupported(classes)); } +// Goal of this test is to verify that multiple pools can be configured +// with defined client classes. +TEST_F(Dhcp6ParserTest, classifyPools) { + ConstElementPtr x; + string config = "{ " + genIfaceConfig() + "," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ { " + " \"pools\": [ { " + " \"pool\": \"2001:db8:1::/80\", " + " \"client-class\": \"alpha\" " + " }," + " {" + " \"pool\": \"2001:db8:2::/80\", " + " \"client-class\": \"beta\" " + " }," + " {" + " \"pool\": \"2001:db8:3::/80\", " + " \"client-class\": \"gamma\" " + " }," + " {" + " \"pool\": \"2001:db8:4::/80\" " + " } ]," + " \"subnet\": \"2001:db8:0::/40\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP6(config, true)); + extractConfig(config); + + EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json)); + checkResult(x, 0); + + const Subnet6Collection* subnets = + CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getAll(); + ASSERT_TRUE(subnets); + ASSERT_EQ(1, subnets->size()); + const PoolCollection& pools = subnets->at(0)->getPools(Lease::TYPE_NA); + ASSERT_EQ(4, pools.size()); // We expect 4 pools + + // Let's check if client belonging to alpha class is supported in pool[0] + // and not supported in any other pool (except pool[3], which allows + // everyone). + ClientClasses classes; + classes.insert("alpha"); + EXPECT_TRUE (pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to beta class is supported in pool[1] + // and not supported in any other pool (except pool[3], which allows + // everyone). + classes.clear(); + classes.insert("beta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_TRUE (pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to gamma class is supported in pool[2] + // and not supported in any other pool (except pool[3], which allows + // everyone). + classes.clear(); + classes.insert("gamma"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_TRUE (pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to some other class (not mentioned in + // the config) is supported only in pool[3], which allows everyone. + classes.clear(); + classes.insert("delta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Finally, let's check class-less client. He should be allowed only in + // the last pool, which does not have any class restrictions. + classes.clear(); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); +} + +// Goal of this test is to verify that multiple pdpools can be configured +// with defined client classes. +TEST_F(Dhcp6ParserTest, classifyPdPools) { + ConstElementPtr x; + string config = "{ " + genIfaceConfig() + "," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ { " + " \"pd-pools\": [ { " + " \"prefix-len\": 48, " + " \"delegated-len\": 64, " + " \"prefix\": \"2001:db8:1::\", " + " \"client-class\": \"alpha\" " + " }," + " {" + " \"prefix-len\": 48, " + " \"delegated-len\": 64, " + " \"prefix\": \"2001:db8:2::\", " + " \"client-class\": \"beta\" " + " }," + " {" + " \"prefix-len\": 48, " + " \"delegated-len\": 64, " + " \"prefix\": \"2001:db8:3::\", " + " \"client-class\": \"gamma\" " + " }," + " {" + " \"prefix-len\": 48, " + " \"delegated-len\": 64, " + " \"prefix\": \"2001:db8:4::\" " + " } ]," + " \"subnet\": \"2001:db8::/64\" " + " } ]," + "\"valid-lifetime\": 4000 }"; + + ConstElementPtr json; + ASSERT_NO_THROW(json = parseDHCP6(config, true)); + extractConfig(config); + + EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json)); + checkResult(x, 0); + + const Subnet6Collection* subnets = + CfgMgr::instance().getStagingCfg()->getCfgSubnets6()->getAll(); + ASSERT_TRUE(subnets); + ASSERT_EQ(1, subnets->size()); + const PoolCollection& pools = subnets->at(0)->getPools(Lease::TYPE_PD); + ASSERT_EQ(4, pools.size()); // We expect 4 pools + + // Let's check if client belonging to alpha class is supported in pool[0] + // and not supported in any other pool (except pool[3], which allows + // everyone). + ClientClasses classes; + classes.insert("alpha"); + EXPECT_TRUE (pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to beta class is supported in pool[1] + // and not supported in any other pool (except pool[3], which allows + // everyone). + classes.clear(); + classes.insert("beta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_TRUE (pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to gamma class is supported in pool[2] + // and not supported in any other pool (except pool[3], which allows + // everyone). + classes.clear(); + classes.insert("gamma"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_TRUE (pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Let's check if client belonging to some other class (not mentioned in + // the config) is supported only in pool[3], which allows everyone. + classes.clear(); + classes.insert("delta"); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); + + // Finally, let's check class-less client. He should be allowed only in + // the last pool, which does not have any class restrictions. + classes.clear(); + EXPECT_FALSE(pools.at(0)->clientSupported(classes)); + EXPECT_FALSE(pools.at(1)->clientSupported(classes)); + EXPECT_FALSE(pools.at(2)->clientSupported(classes)); + EXPECT_TRUE (pools.at(3)->clientSupported(classes)); +} + // This test checks the ability of the server to parse a configuration // containing a full, valid dhcp-ddns (D2ClientConfig) entry. TEST_F(Dhcp6ParserTest, d2ClientConfig) { diff --git a/src/bin/dhcp6/tests/shared_network_unittest.cc b/src/bin/dhcp6/tests/shared_network_unittest.cc index dc8cd2ff31..32fa5d53ec 100644 --- a/src/bin/dhcp6/tests/shared_network_unittest.cc +++ b/src/bin/dhcp6/tests/shared_network_unittest.cc @@ -957,7 +957,151 @@ const char* NETWORKS_CONFIG[] = { " ]" " }" " ]" + "}", + +// 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. @@ -2191,4 +2335,131 @@ 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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &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([this, &client2] { + ASSERT_NO_THROW(client2.doRenew()); + }); + EXPECT_EQ(1, client2.getLeaseNum()); + EXPECT_EQ(1, client2.getLeasesWithNonZeroLifetime().size()); +} + } // end of anonymous namespace -- 2.47.2