]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#1387] Checkpoint: server code
authorFrancis Dupont <fdupont@isc.org>
Tue, 2 Jul 2024 21:36:29 +0000 (23:36 +0200)
committerFrancis Dupont <fdupont@isc.org>
Wed, 4 Sep 2024 13:09:40 +0000 (15:09 +0200)
doc/examples/kea6/all-keys.json
doc/sphinx/arm/dhcp6-srv.rst
src/bin/dhcp6/dhcp6_lexer.ll
src/bin/dhcp6/dhcp6_parser.yy
src/bin/dhcp6/dhcp6_srv.cc
src/bin/dhcp6/dhcp6_srv.h
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
src/bin/dhcp6/tests/renew_unittest.cc
src/bin/dhcp6/tests/sarr_unittest.cc

index 1a7cadc1c818fa9012c45b496deff3e746210cc3..fc793e118b254fdd2b700b077b13a30d7a75d1ce 100644 (file)
                                 // List of reserved IPv6 prefixes.
                                 "prefixes": [ "2001:db8:2:abcd::/64" ],
 
+                                // List of excluded IPv6 prefixes.
+                               "excluded-prefixes": [ "2001:db8:2:abcd:1::/80" ],
+
                                 // Reserved hostname.
                                 "hostname": "foo.example.com",
 
index 2e42d5ddbb6be9372fb45b1ba15b38e617abd25e..5833516d7dcce34c7d699146e498876a455f4b6a 100644 (file)
@@ -4285,8 +4285,26 @@ another.
    An empty ``excluded-prefixes`` list or a list with only empty strings
    can be omitted (and will be omitted when produced by Kea).
 
-   todo: add something about adding the pd-exclude option to returned
-   prefixes as done for prefix delegation pools.
+::
+
+    "reservations": [
+        {
+            "duid": "01:02:03:04:05:06:07:08:09:0A",
+            "ip-addresses": [ "2001:db8:1::103" ],
+            "prefixes": [ "2001:db8::/48 ],
+            "excluded-prefixes": [ "2001:db8:0:1::/64 ],
+            "hostname": "foo.example.com"
+        }
+    ],
+    ...
+
+
+.. note::
+
+   Host reservations have precedence over prefix pools so when a reserved
+   prefix without an excluded prefix is assigned no pd-exclude option
+   is added to the prefix option even the prefix is in a configured
+   prefix pool with an excluded prefix (different from previous behavior).
 
 .. _reservation6-conflict:
 
index de968210fcd9f5ebb96ad7fc58f8375000642403..3646d8e4ae48380b9e7ca1c8be5e5936ff4d50fb 100644 (file)
@@ -1720,6 +1720,15 @@ ControlCharacterFill            [^"\\]|\\["\\/bfnrtu]
     }
 }
 
+\"excluded-prefixes\" {
+    switch(driver.ctx_) {
+    case isc::dhcp::Parser6Context::RESERVATIONS:
+        return isc::dhcp::Dhcp6Parser::make_EXCLUDED_PREFIXES(driver.loc_);
+    default:
+        return isc::dhcp::Dhcp6Parser::make_STRING("excluded-prefixes", driver.loc_);
+    }
+}
+
 \"duid\" {
     switch(driver.ctx_) {
     case isc::dhcp::Parser6Context::MAC_SOURCES:
index a09f50cb72867aab65317e2aeae51bed95588e0f..0247e2df65afaf9c77fc122fbd20ff5f9f29858a 100644 (file)
@@ -189,6 +189,7 @@ using namespace std;
   RESERVATIONS "reservations"
   IP_ADDRESSES "ip-addresses"
   PREFIXES "prefixes"
+  EXCLUDED_PREFIXES "excluded-prefixes"
   DUID "duid"
   HW_ADDRESS "hw-address"
   HOSTNAME "hostname"
@@ -2377,6 +2378,7 @@ reservation_param: duid
                  | reservation_client_classes
                  | ip_addresses
                  | prefixes
+                 | excluded_prefixes
                  | hw_address
                  | hostname
                  | flex_id_value
@@ -2408,6 +2410,17 @@ prefixes: PREFIXES {
     ctx.leave();
 };
 
+excluded_prefixes: EXCLUDED_PREFIXES {
+    ctx.unique("excluded-prefixes", ctx.loc2pos(@1));
+    ElementPtr l(new ListElement(ctx.loc2pos(@1)));
+    ctx.stack_.back()->set("excluded-prefixes", l);
+    ctx.stack_.push_back(l);
+    ctx.enter(ctx.NO_KEYWORD);
+} COLON list_strings {
+    ctx.stack_.pop_back();
+    ctx.leave();
+};
+
 duid: DUID {
     ctx.unique("duid", ctx.loc2pos(@1));
     ctx.enter(ctx.NO_KEYWORD);
index fcf11f9b7995bc7ef85ef57ffcd5846b0466616e..750eac9d861d992596bbd0a44850d7cdf1fcfc24 100644 (file)
@@ -2462,6 +2462,32 @@ Dhcpv6Srv::getMAC(const Pkt6Ptr& pkt) {
     return (hwaddr);
 }
 
+OptionPtr
+Dhcpv6Srv::getPDExclude(const AllocEngine::ClientContext6& ctx,
+                        const Lease6Ptr& lease) {
+
+    // Search the reservation the prefix is from.
+    ConstHostPtr host = ctx.currentHost();
+    if (host) {
+        IPv6ResrvRange resvs = host->getIPv6Reservations(IPv6Resrv::TYPE_PD);
+        BOOST_FOREACH(auto const& resv, resvs) {
+            if ((resv.second.getPrefix() == lease->addr_) &&
+                (resv.second.getPrefixLen() == lease->prefixlen_)) {
+                return (resv.second.getPDExclude());
+            }
+        }
+    }
+
+    // Search the pool the address is from.
+    const Subnet6Ptr& subnet = ctx.subnet_;
+    Pool6Ptr pool = boost::dynamic_pointer_cast<Pool6>(
+        subnet->getPool(Lease::TYPE_PD, lease->addr_));
+    if (pool) {
+        return (pool->getPrefixExcludeOption());
+    }
+    return (OptionPtr());
+}
+
 OptionPtr
 Dhcpv6Srv::assignIA_NA(const Pkt6Ptr& query,
                        AllocEngine::ClientContext6& ctx,
@@ -2718,15 +2744,9 @@ Dhcpv6Srv::assignIA_PD(const Pkt6Ptr& query,
             ia_rsp->addOption(addr);
 
             if (pd_exclude_requested) {
-                // PD exclude option has been requested via ORO, thus we need to
-                // include it if the pool configuration specifies this option.
-                Pool6Ptr pool = boost::dynamic_pointer_cast<
-                    Pool6>(subnet->getPool(Lease::TYPE_PD, l->addr_));
-                if (pool) {
-                    Option6PDExcludePtr pd_exclude_option = pool->getPrefixExcludeOption();
-                    if (pd_exclude_option) {
-                        addr->addOption(pd_exclude_option);
-                    }
+                OptionPtr pd_exclude_option = getPDExclude(ctx, l);
+                if (pd_exclude_option) {
+                    addr->addOption(pd_exclude_option);
                 }
             }
         }
@@ -3049,16 +3069,9 @@ Dhcpv6Srv::extendIA_PD(const Pkt6Ptr& query,
         ia_rsp->addOption(prf);
 
         if (pd_exclude_requested) {
-            // PD exclude option has been requested via ORO, thus we need to
-            // include it if the pool configuration specifies this option.
-            Pool6Ptr pool = boost::dynamic_pointer_cast<
-                Pool6>(subnet->getPool(Lease::TYPE_PD, l->addr_));
-
-            if (pool) {
-                Option6PDExcludePtr pd_exclude_option = pool->getPrefixExcludeOption();
-                if (pd_exclude_option) {
-                    prf->addOption(pd_exclude_option);
-                }
+            OptionPtr pd_exclude_option = getPDExclude(ctx, l);
+            if (pd_exclude_option) {
+                prf->addOption(pd_exclude_option);
             }
         }
 
index 13dc444ba2a939aa927b09b2c1d520ffb79daf88..3ef6e9cff4c3621a3c23801814842eca50dd90b0 100644 (file)
@@ -1059,6 +1059,14 @@ protected:
     void checkDynamicSubnetChange(const Pkt6Ptr& question, Pkt6Ptr& answer,
                                   AllocEngine::ClientContext6& ctx,
                                   const Subnet6Ptr orig_subnet);
+
+    /// @brief Return the PD exclude option to include.
+    ///
+    /// @param ctx client context (contains subnet and hosts).
+    /// @param lease lease (contains address/prefix and prefix length).
+    OptionPtr getPDExclude(const AllocEngine::ClientContext6& ctx,
+                           const Lease6Ptr& lease);
+
 public:
 
     /// Used for DHCPv4-over-DHCPv6 too.
index c84a44be60a9198ea61d8d2a0115a0c8f82f0d70..9904810fc535b1ed60ecf78ffe74760443e771c0 100644 (file)
@@ -4050,7 +4050,7 @@ TEST_F(Dhcpv6SrvTest, generateFqdnUpdate) {
     EXPECT_EQ("myhost-2001-db8-1--1.example.com.", l->hostname_);
 
     // Should see follow log message ids in the log file.
-    EXPECT_EQ(1, countFile("DHCP6_DDNS_FQDN_GENERATED")); 
+    EXPECT_EQ(1, countFile("DHCP6_DDNS_FQDN_GENERATED"));
     EXPECT_EQ(0, countFile("DHCP6_DDNS_GENERATED_FQDN_UPDATE_FAIL"));
 }
 
@@ -4104,7 +4104,7 @@ TEST_F(Dhcpv6SrvTest, generateFqdnNoUpdate) {
 
     // Check that an IA_NA with an iaaddr was returned for the requested
     // address with lifetimes of 0.
-    boost::shared_ptr<Option6IAAddr> iaaddr = checkIA_NA(reply, 234, 0, 0); 
+    boost::shared_ptr<Option6IAAddr> iaaddr = checkIA_NA(reply, 234, 0, 0);
     ASSERT_TRUE(iaaddr);
     EXPECT_EQ(addr, iaaddr->getAddress());
     EXPECT_EQ(0, iaaddr->getPreferred());
index e20867b18301dea8779b7efae9ec75452fcec8bc..01bb3fae8acfc474493a614fb8856c740f19a5ec 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2023 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2015-2024 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -61,6 +61,22 @@ namespace {
 ///   - prefix pool: 3000::/72
 ///   - excluded prefix 3000::1000/120 in a prefix pool.
 ///
+/// - Configuration 6:
+///   - addresses and prefixes
+///   - 1 subnet with one address pool and one prefix pool
+///   - address pool: 2001:db8:1::/64
+///   - prefix pool: 3000::/72
+///   - excluded prefix 3000::1000/120 in a prefix pool.
+///   - reservation for 3000::/80 (has precedence over the prefix pool).
+///
+/// - Configuration 7:
+///   - addresses and prefixes
+///   - 1 subnet with one address pool and one prefix pool
+///   - address pool: 2001:db8:1::/64
+///   - prefix pool: 3000::/72
+///   - excluded prefix 3000::1000/120 in a prefix pool.
+///   - reservation for 3000::/80 with 3000::2000/120 excluded prefix.
+///
 const char* RENEW_CONFIGS[] = {
 // Configuration 0
     "{ \"interfaces-config\": {"
@@ -224,7 +240,67 @@ const char* RENEW_CONFIGS[] = {
         "    \"interface-id\": \"\","
         "    \"interface\": \"eth0\""
         " } ],"
-        "\"valid-lifetime\": 4000 }"
+        "\"valid-lifetime\": 4000"
+    "}",
+
+// Configuration 6
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"id\": 1, "
+        "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
+        "    \"pd-pools\": ["
+        "        { \"prefix\": \"3000::\", "
+        "          \"prefix-len\": 72, "
+        "          \"delegated-len\": 80,"
+        "          \"excluded-prefix\": \"3000::1000\","
+        "          \"excluded-prefix-len\": 120"
+        "        } ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"reservations\": ["
+        "    {"
+        "        \"duid\": \"01:02:03:05\","
+        "        \"prefixes\": [ \"3000::/80\" ]"
+        "    } ],"
+        "    \"interface-id\": \"\","
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
+    "}",
+
+// Configuration 7
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"id\": 1, "
+        "    \"pools\": [ { \"pool\": \"2001:db8:1::/64\" } ],"
+        "    \"pd-pools\": ["
+        "        { \"prefix\": \"3000::\", "
+        "          \"prefix-len\": 72, "
+        "          \"delegated-len\": 80,"
+        "          \"excluded-prefix\": \"3000::1000\","
+        "          \"excluded-prefix-len\": 120"
+        "        } ],"
+        "    \"subnet\": \"2001:db8:1::/48\", "
+        "    \"reservations\": ["
+        "    {"
+        "        \"duid\": \"01:02:03:05\","
+        "        \"prefixes\": [ \"3000::/80\" ],"
+        "        \"excluded-prefixes\": [ \"3000::2000/120\" ]"
+        "    } ],"
+        "    \"interface-id\": \"\","
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
+    "}"
 };
 
 /// @brief Test fixture class for testing Renew.
@@ -314,8 +390,8 @@ TEST_F(RenewTest, requestPrefixInRenew) {
 }
 
 // Test that it is possible to renew a prefix lease with a Prefix Exclude
-// option being included during renew.
-TEST_F(RenewTest, renewWithExcludedPrefix) {
+// option from PD pool being included during renew.
+TEST_F(RenewTest, renewWithExcludedPrefixPool) {
     Dhcp6Client client;
 
     // Configure client to request IA_NA and IA_PD.
@@ -325,7 +401,7 @@ TEST_F(RenewTest, renewWithExcludedPrefix) {
     // Request Prefix Exclude option.
     client.requestOption(D6O_PD_EXCLUDE);
 
-    // Configure the server with NA pools only.
+    // Configure the server with NA and PD pools but without excluded prefix.
     ASSERT_NO_THROW(configure(RENEW_CONFIGS[2], *client.getServer()));
 
     // Perform 4-way exchange.
@@ -399,6 +475,95 @@ TEST_F(RenewTest, renewWithExcludedPrefix) {
     EXPECT_EQ(120, static_cast<unsigned>(pd_exclude->getExcludedPrefixLength()));
 }
 
+// Test that it is possible to renew a prefix lease with a Prefix Exclude
+// option from host reservation being included during renew.
+TEST_F(RenewTest, renewWithExcludedPrefixHost) {
+    Dhcp6Client client;
+
+    // Set DUID matching the one used to create host reservations.
+    client.setDUID("01:02:03:05");
+
+    // Configure client to request IA_NA and IA_PD.
+    client.requestAddress(na_iaid_);
+    client.requestPrefix(pd_iaid_);
+
+    // Request Prefix Exclude option.
+    client.requestOption(D6O_PD_EXCLUDE);
+
+    // Configure the server with a reservation but without excluded prefix.
+    ASSERT_NO_THROW(configure(RENEW_CONFIGS[6], *client.getServer()));
+
+    // Perform 4-way exchange.
+    ASSERT_NO_THROW(client.doSARR());
+
+    // Simulate aging of leases.
+    client.fastFwdTime(1000);
+
+    // Make sure that the client has acquired NA lease.
+    std::vector<Lease6> leases_client_na = client.getLeasesByType(Lease::TYPE_NA);
+    ASSERT_EQ(1, leases_client_na.size());
+    EXPECT_EQ(STATUS_Success, client.getStatusCode(na_iaid_));
+
+    // The client should also acquire a PD lease.
+    std::vector<Lease6> leases_client_pd = client.getLeasesByType(Lease::TYPE_PD);
+    ASSERT_EQ(1, leases_client_pd.size());
+    ASSERT_EQ(STATUS_Success, client.getStatusCode(pd_iaid_));
+
+    // Send Renew message to the server, including IA_NA and IA_PD.
+    ASSERT_NO_THROW(client.doRenew());
+
+    std::vector<Lease6> leases_client_na_renewed =
+        client.getLeasesByType(Lease::TYPE_NA);
+    ASSERT_EQ(1, leases_client_na_renewed.size());
+    EXPECT_EQ(STATUS_Success, client.getStatusCode(na_iaid_));
+
+    std::vector<Lease6> leases_client_pd_renewed =
+        client.getLeasesByType(Lease::TYPE_PD);
+    ASSERT_EQ(1, leases_client_pd_renewed.size());
+    EXPECT_EQ(STATUS_Success, client.getStatusCode(pd_iaid_));
+
+    // Make sure that Prefix Exclude option hasn't been included.
+    OptionPtr option = client.getContext().response_->getOption(D6O_IA_PD);
+    ASSERT_TRUE(option);
+    option = option->getOption(D6O_IAPREFIX);
+    ASSERT_TRUE(option);
+    option = option->getOption(D6O_PD_EXCLUDE);
+    ASSERT_FALSE(option);
+
+    // Reconfigure the server to use the reservation with excluded prefix.
+    configure(RENEW_CONFIGS[7], *client.getServer());
+
+    // Send Renew message to the server, including IA_NA and IA_PD.
+    ASSERT_NO_THROW(client.doRenew());
+
+    // Make sure that the client has acquired NA lease.
+    leases_client_na_renewed = client.getLeasesByType(Lease::TYPE_NA);
+    ASSERT_EQ(1, leases_client_na_renewed.size());
+    EXPECT_EQ(STATUS_Success, client.getStatusCode(na_iaid_));
+
+    // Make sure that the client has acquired PD lease.
+    leases_client_pd_renewed = client.getLeasesByType(Lease::TYPE_PD);
+    ASSERT_EQ(1, leases_client_pd_renewed.size());
+    EXPECT_EQ(STATUS_Success, client.getStatusCode(pd_iaid_));
+
+    // The leases should have been renewed.
+    EXPECT_GE(leases_client_na_renewed[0].cltt_ - leases_client_na[0].cltt_, 1000);
+    EXPECT_GE(leases_client_pd_renewed[0].cltt_ - leases_client_pd[0].cltt_, 1000);
+
+    // This time, the Prefix Exclude option should be included.
+    option = client.getContext().response_->getOption(D6O_IA_PD);
+    ASSERT_TRUE(option);
+    option = option->getOption(D6O_IAPREFIX);
+    ASSERT_TRUE(option);
+    option = option->getOption(D6O_PD_EXCLUDE);
+    ASSERT_TRUE(option);
+    Option6PDExcludePtr pd_exclude = boost::dynamic_pointer_cast<Option6PDExclude>(option);
+    ASSERT_TRUE(pd_exclude);
+    EXPECT_EQ("3000::2000", pd_exclude->getExcludedPrefix(IOAddress("3000::"),
+                                                          80).toText());
+    EXPECT_EQ(120, static_cast<unsigned>(pd_exclude->getExcludedPrefixLength()));
+}
+
 // This test verifies that the client can request a prefix delegation
 // with a hint, while it is renewing an address lease.
 TEST_F(RenewTest, requestPrefixInRenewUseHint) {
index 7c41472892529f60aced6b3eee776e4f3ac15f0d..074b54857d89adce95fb183df5950ad642640848 100644 (file)
@@ -74,6 +74,16 @@ namespace {
 ///   - One subnet with three distinct pools.
 ///   - Random allocator enabled globally for delegated prefixes.
 ///   - Iterative allocator for address allocation.
+///
+/// - Configation 7:
+///   - Cache max age and threshold.
+///
+/// - Configuration 8 (derived from 3):
+///   - one subnet 3000::/32 used on eth0 interface
+///   - prefixes of length 64, delegated from the pool: 2001:db8:3::/48
+///   - Excluded Prefix specified (RFC 6603).
+///   - Reservation (which has precedence over the pool) with excluded prefix.
+///
 const char* CONFIGS[] = {
     // Configuration 0
     "{ \"interfaces-config\": {"
@@ -328,6 +338,35 @@ const char* CONFIGS[] = {
         ],
         "valid-lifetime": 600
     })",
+
+    // Configuration 8
+    "{ \"interfaces-config\": {"
+        "  \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"id\": 1, "
+        "    \"pd-pools\": ["
+        "        { \"prefix\": \"2001:db8:3::\", "
+        "          \"prefix-len\": 48, "
+        "          \"delegated-len\": 64,"
+        "          \"excluded-prefix\": \"2001:db8:3::1000\","
+        "          \"excluded-prefix-len\": 120"
+        "        } ],"
+        "    \"subnet\": \"3000::/32\", "
+        "    \"reservations\": ["
+        "    {"
+        "        \"duid\": \"01:02:03:05\","
+        "        \"prefixes\": [ \"2001:db8:3::/64\" ],"
+        "        \"excluded-prefixes\": [ \"2001:db8:3::2000/120\" ]"
+        "    } ],"
+        "    \"interface-id\": \"\","
+        "    \"interface\": \"eth0\""
+        " } ],"
+        "\"valid-lifetime\": 4000"
+    "}",
 };
 
 /// @brief Test fixture class for testing 4-way exchange: Solicit-Advertise,
@@ -379,8 +418,13 @@ public:
 
     /// @brief This test verifies that it is possible to specify an excluded
     /// prefix (RFC 6603) and send it back to the client requesting prefix
-    /// delegation.
-    void directClientExcludedPrefix();
+    /// delegation using a pool.
+    void directClientExcludedPrefixPool();
+
+    /// @brief This test verifies that it is possible to specify an excluded
+    /// prefix (RFC 6603) and send it back to the client requesting prefix
+    /// delegation using a reservation.
+    void directClientExcludedPrefixHost();
 
     /// @brief Check that when the client includes the Rapid Commit option in
     /// its Solicit, the server responds with Reply and commits the lease.
@@ -638,7 +682,7 @@ TEST_F(SARRTest, optionsInheritanceMultiThreading) {
 }
 
 void
-SARRTest::directClientExcludedPrefix() {
+SARRTest::directClientExcludedPrefixPool() {
     Dhcp6Client client;
     // Configure client to request IA_PD.
     client.requestPrefix();
@@ -677,14 +721,66 @@ SARRTest::directClientExcludedPrefix() {
     EXPECT_EQ(120, static_cast<unsigned>(pd_exclude->getExcludedPrefixLength()));
 }
 
-TEST_F(SARRTest, directClientExcludedPrefix) {
+TEST_F(SARRTest, directClientExcludedPrefixPool) {
+    Dhcpv6SrvMTTestGuard guard(*this, false);
+    directClientExcludedPrefixPool();
+}
+
+TEST_F(SARRTest, directClientExcludedPrefixPoolMultiThreading) {
+    Dhcpv6SrvMTTestGuard guard(*this, true);
+    directClientExcludedPrefixPool();
+}
+
+void
+SARRTest::directClientExcludedPrefixHost() {
+    Dhcp6Client client;
+    // Set DUID matching the one used to create host reservations.
+    client.setDUID("01:02:03:05");
+    // Configure client to request IA_PD.
+    client.requestPrefix();
+    client.requestOption(D6O_PD_EXCLUDE);
+    configure(CONFIGS[8], *client.getServer());
+    // Make sure we ended-up having expected number of subnets configured.
+    const Subnet6Collection* subnets = CfgMgr::instance().getCurrentCfg()->
+        getCfgSubnets6()->getAll();
+    ASSERT_EQ(1, subnets->size());
+    // Perform 4-way exchange.
+    ASSERT_NO_THROW(client.doSARR());
+    // Server should have assigned a prefix.
+    ASSERT_EQ(1, client.getLeaseNum());
+    Lease6 lease_client = client.getLease(0);
+    EXPECT_EQ(64, lease_client.prefixlen_);
+    EXPECT_EQ(3000, lease_client.preferred_lft_);
+    EXPECT_EQ(4000, lease_client.valid_lft_);
+    Lease6Ptr lease_server = checkLease(lease_client);
+    // Check that the server recorded the lease.
+    ASSERT_TRUE(lease_server);
+
+    OptionPtr option = client.getContext().response_->getOption(D6O_IA_PD);
+    ASSERT_TRUE(option);
+    Option6IAPtr ia = boost::dynamic_pointer_cast<Option6IA>(option);
+    ASSERT_TRUE(ia);
+    option = ia->getOption(D6O_IAPREFIX);
+    ASSERT_TRUE(option);
+    Option6IAPrefixPtr pd_option = boost::dynamic_pointer_cast<Option6IAPrefix>(option);
+    ASSERT_TRUE(pd_option);
+    option = pd_option->getOption(D6O_PD_EXCLUDE);
+    ASSERT_TRUE(option);
+    Option6PDExcludePtr pd_exclude = boost::dynamic_pointer_cast<Option6PDExclude>(option);
+    ASSERT_TRUE(pd_exclude);
+    EXPECT_EQ("2001:db8:3::2000", pd_exclude->getExcludedPrefix(IOAddress("2001:db8:3::"),
+                                                                64).toText());
+    EXPECT_EQ(120, static_cast<unsigned>(pd_exclude->getExcludedPrefixLength()));
+}
+
+TEST_F(SARRTest, directClientExcludedPrefixHost) {
     Dhcpv6SrvMTTestGuard guard(*this, false);
-    directClientExcludedPrefix();
+    directClientExcludedPrefixHost();
 }
 
-TEST_F(SARRTest, directClientExcludedPrefixMultiThreading) {
+TEST_F(SARRTest, directClientExcludedPrefixHostMultiThreading) {
     Dhcpv6SrvMTTestGuard guard(*this, true);
-    directClientExcludedPrefix();
+    directClientExcludedPrefixHost();
 }
 
 void