Parcourir la source

[master] Merge branch 'trac5005'

Marcin Siodelski il y a 8 ans
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">
     <section id="reservation4-hostname">
       <title>Reserving a hostname</title>
       <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
       precedence over the hostname supplied by the client or the autogenerated
       (from the IPv4 address) hostname.</para>
       (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
 transaction identification information. The second argument specifies
 the leased address.
 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
 % 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
 A debug message including the detailed data about the packet being sent
 to the client. The first argument contains the client and the transaction
 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.
     // Fetch D2 configuration.
     D2ClientMgr& d2_mgr = CfgMgr::instance().getD2ClientMgr();
     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 =
     D2ClientConfig::ReplaceClientNameMode replace_name_mode =
             d2_mgr.getD2ClientConfig()->getReplaceClientNameMode();
             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 we don't have a hostname then either we'll supply it or do nothing.
     if (!opt_hostname) {
     if (!opt_hostname) {
         // If we're configured to supply it then add it to the response.
         // 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
     // By checking the number of labels present in the hostname we may infer
     // whether client has sent the fully qualified or unqualified hostname.
     // 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.
         // Set to root domain to signal later on that we should replace it.
         // DHO_HOST_NAME is a string option which cannot be empty.
         // DHO_HOST_NAME is a string option which cannot be empty.
         /// @todo We may want to reconsider whether it is appropriate for the
         /// @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.
     /// @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
     /// @param ex The exchange holding both the client's message and the
     /// server's response.
     /// server's response.

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

@@ -108,6 +108,60 @@ const char* CONFIGS[] = {
             "\"enable-updates\": true,"
             "\"enable-updates\": true,"
             "\"qualifying-suffix\": \"fake-suffix.isc.org.\""
             "\"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
 // 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
 // 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
 // verifies that the lease is only in the database following a DHCPREQUEST and