Browse Source

[master] Merge branch 'trac5005'

Marcin Siodelski 8 years ago
parent
commit
96ab8ecaa3

+ 9 - 4
doc/guide/dhcp4-srv.xml

@@ -2775,10 +2775,15 @@ It is merely echoed by the server
 
     <section id="reservation4-hostname">
       <title>Reserving a hostname</title>
-      <para>When the reservation for the client includes the <command>hostname
-      </command>, the server will assign this hostname to the client and send
-      it back in the Client FQDN or Hostname option, depending on which of them
-      the client has sent to the server. The reserved hostname always takes
+      <para>When the reservation for a client includes the <command>hostname
+      </command>, the server will return this hostname to the client in
+      the Client FQDN or Hostname option. The server responds with the Client
+      FQDN option only if the client has included Client FQDN option in its
+      message to the server. The server will respond with the Hostname option
+      if the client has included Hostname option in its message to the server,
+      or when the client has requested Hostname option using Parameter Request
+      List option. The server will return Hostname option even if it is not
+      configured to perform DNS updates. The reserved hostname always takes
       precedence over the hostname supplied by the client or the autogenerated
       (from the IPv4 address) hostname.</para>
 

+ 6 - 0
src/bin/dhcp4/dhcp4_messages.mes

@@ -600,6 +600,12 @@ and the lease. The first argument includes the client and the
 transaction identification information. The second argument specifies
 the leased address.
 
+% DHCP4_RESERVED_HOSTNAME_ASSIGNED %1: server assigned reserved hostname %2
+This debug message is issued when the server found a hostname reservation
+for a client and uses this reservation in a hostname option sent back
+to this client. The reserved hostname is qualified with a value
+of 'qualifying-suffix' parameter, if this parameter is specified.
+
 % DHCP4_RESPONSE_DATA %1: responding with packet %2 (type %3), packet details: %4
 A debug message including the detailed data about the packet being sent
 to the client. The first argument contains the client and the transaction

+ 69 - 14
src/bin/dhcp4/dhcp4_srv.cc

@@ -1400,18 +1400,77 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
     // Fetch D2 configuration.
     D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
 
-    // Do nothing if the DNS updates are disabled.
-    if (!d2_mgr.ddnsEnabled()) {
-        return;
+    // Obtain the Hostname option from the client's message.
+    OptionStringPtr opt_hostname = boost::dynamic_pointer_cast<OptionString>
+        (ex.getQuery()->getOption(DHO_HOST_NAME));
+
+    if (opt_hostname) {
+        LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_CLIENT_HOSTNAME_DATA)
+            .arg(ex.getQuery()->getLabel())
+            .arg(opt_hostname->getValue());
     }
 
+    AllocEngine::ClientContext4Ptr ctx = ex.getContext();
+
+    // Hostname reservations take precedence over any other configuration,
+    // i.e. DDNS configuration.
+    if (ctx->host_ && !ctx->host_->getHostname().empty()) {
+        // In order to send a reserved hostname value we expect that at least
+        // one of the following is the case: the client has sent us a hostname
+        // option, or the client has sent Parameter Request List option with
+        // the hostname option code included.
+
+        // It is going to be less expensive to first check the presence of the
+        // hostname option.
+        bool should_send_hostname = static_cast<bool>(opt_hostname);
+        // Hostname option is not present, so we have to check PRL option.
+        if (!should_send_hostname) {
+            OptionUint8ArrayPtr
+                option_prl = boost::dynamic_pointer_cast<OptionUint8Array>
+                (ex.getQuery()->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+            if (option_prl) {
+                // PRL option exists, so check if the hostname option code is
+                // included in it.
+                const std::vector<uint8_t>&
+                    requested_opts = option_prl->getValues();
+                if (std::find(requested_opts.begin(), requested_opts.end(),
+                              DHO_HOST_NAME) != requested_opts.end()) {
+                    // Client has requested hostname via Parameter Request
+                    // List option.
+                    should_send_hostname = true;
+                }
+            }
+        }
+
+        // If the hostname or PRL option indicates that the server should
+        // send back a hostname option, send this option with a reserved
+        // name for this client.
+        if (should_send_hostname) {
+            const std::string& hostname = d2_mgr.qualifyName(ctx->host_->getHostname(),
+                                                             false);
+            LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL_DATA,
+                      DHCP4_RESERVED_HOSTNAME_ASSIGNED)
+                .arg(ex.getQuery()->getLabel())
+                .arg(hostname);
+            OptionStringPtr opt_hostname_resp(new OptionString(Option::V4,
+                                                               DHO_HOST_NAME,
+                                                               hostname));
+            ex.getResponse()->addOption(opt_hostname_resp);
+
+            // We're done here.
+            return;
+        }
+    }
+
+    // There is no reservation for this client or the client hasn't requested
+    // hostname option. There is still a possibility that we'll have to send
+    // hostname option to this client if the client has included hostname option
+    // but there is no reservation, or the configuration of the server requires
+    // that we send the option regardless.
+
     D2ClientConfig::ReplaceClientNameMode replace_name_mode =
             d2_mgr.getD2ClientConfig()->getReplaceClientNameMode();
 
-    // Obtain the Hostname option from the client's message.
-    OptionStringPtr opt_hostname = boost::dynamic_pointer_cast<OptionString>
-        (ex.getQuery()->getOption(DHO_HOST_NAME));
-
     // If we don't have a hostname then either we'll supply it or do nothing.
     if (!opt_hostname) {
         // If we're configured to supply it then add it to the response.
@@ -1459,14 +1518,10 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
     // By checking the number of labels present in the hostname we may infer
     // whether client has sent the fully qualified or unqualified hostname.
 
-    // If there is a hostname reservation for this client, use it.
-    if (ex.getContext()->host_ && !ex.getContext()->host_->getHostname().empty()) {
-        opt_hostname_resp->setValue(d2_mgr.qualifyName(ex.getContext()->host_->getHostname(),
-                                                       false));
 
-    } else if ((replace_name_mode == D2ClientConfig::RCM_ALWAYS ||
-               replace_name_mode == D2ClientConfig::RCM_WHEN_PRESENT)
-               || label_count < 2) {
+    if ((replace_name_mode == D2ClientConfig::RCM_ALWAYS ||
+         replace_name_mode == D2ClientConfig::RCM_WHEN_PRESENT)
+        || label_count < 2) {
         // Set to root domain to signal later on that we should replace it.
         // DHO_HOST_NAME is a string option which cannot be empty.
         /// @todo We may want to reconsider whether it is appropriate for the

+ 26 - 5
src/bin/dhcp4/dhcp4_srv.h

@@ -592,11 +592,32 @@ private:
 
     /// @brief Process Hostname %Option sent by a client.
     ///
-    /// This function is called by the @c Dhcpv4Srv::processClientName when
-    /// the client has sent the Hostname option in its message to the server.
-    /// It comprises the actual logic to parse the Hostname option and
-    /// prepare the Hostname option to be sent back to the client in the
-    /// server's response.
+    /// This method is called by the @c Dhcpv4Srv::processClientName to
+    /// create an instance of the Hostname option to be returned to the
+    /// client. If this instance is created it is included in the response
+    /// message within the @c Dhcpv4Exchange object passed as an argument.
+    ///
+    /// The Hostname option instance is created if the client has included
+    /// Hostname option in its query to the server or if the client has
+    /// included Hostname option code in the Parameter Request List option.
+    /// In the former case, the server can use the Hostname supplied by the
+    /// client or replace it with a new hostname, depending on the server's
+    /// configuration. A reserved hostname takes precedence over a hostname
+    /// supplied by the client or auto generated hostname.
+    ///
+    /// If the 'qualifying-suffix' parameter is specified, its value is used
+    /// to qualify a hostname. For example, if the host reservation contains
+    /// a hostname 'marcin-laptop', and the qualifying suffix is
+    /// 'example.isc.org', the hostname returned to the client will be
+    /// 'marcin-laptop.example.isc.org'. If the 'qualifying-suffix' is not
+    /// specified (empty), the reserved hostname is returned to the client
+    /// unqualified.
+    ///
+    /// The 'qualifying-suffix' value is also used to qualify the hostname
+    /// supplied by the client, when this hostname is unqualified,
+    /// e.g. 'laptop-x'. If the supplied hostname is qualified, e.g.
+    /// 'laptop-x.example.org', the qualifying suffix will not be appended
+    /// to it.
     ///
     /// @param ex The exchange holding both the client's message and the
     /// server's response.

+ 135 - 0
src/bin/dhcp4/tests/fqdn_unittest.cc

@@ -108,6 +108,60 @@ const char* CONFIGS[] = {
             "\"enable-updates\": true,"
             "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
         "}"
+    "}",
+    // Configuration which disables DNS updates but contains a reservation
+    // for a hostname. Reserved hostname should be assigned to a client if
+    // the client includes it in the Parameter Request List option.
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 3000,"
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"10.0.0.0/24\", "
+        "    \"id\": 1,"
+        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+        "    \"option-data\": [ {"
+        "        \"name\": \"routers\","
+        "        \"data\": \"10.0.0.200,10.0.0.201\""
+        "    } ],"
+        "    \"reservations\": ["
+        "       {"
+        "         \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "         \"hostname\":   \"reserved.example.org\""
+        "       }"
+        "    ]"
+        " }],"
+        "\"dhcp-ddns\": {"
+            "\"enable-updates\": false,"
+            "\"qualifying-suffix\": \"\""
+        "}"
+    "}",
+    // Configuration which disables DNS updates but contains a reservation
+    // for a hostname and the qualifying-suffix which should be appended to
+    // the reserved hostname in the Hostname option returned to a client.
+    "{ \"interfaces-config\": {"
+        "      \"interfaces\": [ \"*\" ]"
+        "},"
+        "\"valid-lifetime\": 3000,"
+        "\"subnet4\": [ { "
+        "    \"subnet\": \"10.0.0.0/24\", "
+        "    \"id\": 1,"
+        "    \"pools\": [ { \"pool\": \"10.0.0.10-10.0.0.100\" } ],"
+        "    \"option-data\": [ {"
+        "        \"name\": \"routers\","
+        "        \"data\": \"10.0.0.200,10.0.0.201\""
+        "    } ],"
+        "    \"reservations\": ["
+        "       {"
+        "         \"hw-address\": \"aa:bb:cc:dd:ee:ff\","
+        "         \"hostname\":   \"foo-bar\""
+        "       }"
+        "    ]"
+        " }],"
+        "\"dhcp-ddns\": {"
+            "\"enable-updates\": false,"
+            "\"qualifying-suffix\": \"example.isc.org\""
+        "}"
     "}"
 };
 
@@ -1324,6 +1378,87 @@ TEST_F(NameDhcpv4SrvTest, hostnameReservation) {
     }
 }
 
+// This test verifies that the server sends the Hostname option to the client
+// with hostname reservation and which included hostname option code in the
+// Parameter Request List.
+TEST_F(NameDhcpv4SrvTest, hostnameReservationPRL) {
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Use HW address that matches the reservation entry in the configuration.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    // Configure DHCP server.
+    configure(CONFIGS[4], *client.getServer());
+    // Make sure that DDNS is enabled.
+    ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+    // Request Hostname option.
+    ASSERT_NO_THROW(client.requestOption(DHO_HOST_NAME));
+
+    // Send the DHCPDISCOVER
+    ASSERT_NO_THROW(client.doDiscover());
+
+    // Make sure that the server responded.
+    Pkt4Ptr resp = client.getContext().response_;
+    ASSERT_TRUE(resp);
+    ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+    // Obtain the Hostname option sent in the response and make sure that the server
+    // has used the hostname reserved for this client.
+    OptionStringPtr hostname;
+    hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("reserved.example.org", hostname->getValue());
+
+    // Now send the DHCPREQUEST with including the Hostname option.
+    ASSERT_NO_THROW(client.doRequest());
+    resp = client.getContext().response_;
+    ASSERT_TRUE(resp);
+    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+    // Once again check that the Hostname is as expected.
+    hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("reserved.example.org", hostname->getValue());
+}
+
+// This test verifies that the server sends the Hostname option to the client
+// with partial hostname reservation and with the global qualifying-suffix set.
+TEST_F(NameDhcpv4SrvTest, hostnameReservationNoDNSQualifyingSuffix) {
+    Dhcp4Client client(Dhcp4Client::SELECTING);
+    // Use HW address that matches the reservation entry in the configuration.
+    client.setHWAddress("aa:bb:cc:dd:ee:ff");
+    // Configure DHCP server.
+    configure(CONFIGS[5], *client.getServer());
+    // Make sure that DDNS is enabled.
+    ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+    // Include the Hostname option.
+    ASSERT_NO_THROW(client.includeHostname("client-name"));
+
+    // Send the DHCPDISCOVER
+    ASSERT_NO_THROW(client.doDiscover());
+
+    // Make sure that the server responded.
+    Pkt4Ptr resp = client.getContext().response_;
+    ASSERT_TRUE(resp);
+    ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+    // Obtain the Hostname option sent in the response and make sure that the server
+    // has used the hostname reserved for this client.
+    OptionStringPtr hostname;
+    hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("foo-bar.example.isc.org", hostname->getValue());
+
+    // Now send the DHCPREQUEST with including the Hostname option.
+    ASSERT_NO_THROW(client.doRequest());
+    resp = client.getContext().response_;
+    ASSERT_TRUE(resp);
+    ASSERT_EQ(DHCPACK, static_cast<int>(resp->getType()));
+
+    // Once again check that the Hostname is as expected.
+    hostname = boost::dynamic_pointer_cast<OptionString>(resp->getOption(DHO_HOST_NAME));
+    ASSERT_TRUE(hostname);
+    EXPECT_EQ("foo-bar.example.isc.org", hostname->getValue());
+}
+
 // Test verifies that the server properly generates a FQDN when the client
 // FQDN name is blank, whether or not DDNS updates are enabled.  It also
 // verifies that the lease is only in the database following a DHCPREQUEST and