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