Browse Source

[2596] "Interface" parameter in Subnet6 configuration added.

Tomek Mrugalski 12 years ago
parent
commit
d47e9e008a

+ 6 - 0
ChangeLog

@@ -1,3 +1,9 @@
+5XX.	[func]		tomek
+	b10-dhcp6: It is now possible to specify that a configured subnet
+	is reachable locally over specified interface (see "interface"
+	parameter in Subnet6 configuration).
+	(Trac #2596, git TBD)
+
 545.	[func]		jinmei
 545.	[func]		jinmei
 	libdns++: the SOA Rdata class now uses the generic lexer in
 	libdns++: the SOA Rdata class now uses the generic lexer in
 	constructors from text.  This means that the MNAME and RNAME of an
 	constructors from text.  This means that the MNAME and RNAME of an

+ 26 - 0
doc/guide/bind10-guide.xml

@@ -3839,6 +3839,32 @@ Dhcp6/subnet6	         []     list    (default)</screen>
           and problems in the DHCPv6 server. See <xref linkend="dhcp6-limit"/>.
           and problems in the DHCPv6 server. See <xref linkend="dhcp6-limit"/>.
         </para>
         </para>
       </note>
       </note>
+      <section id="dhcp6-config-subnets">
+        <title>Subnet selection</title>
+        <para>
+          DHCPv6 server may receive requests from local (connected to the same
+          subnet as the server) and remote (connecting via relays)
+          clients. Server may have many subnet configurations defined, so it
+          must select appropriate subnet for a given request. Server first
+          checks if there is only one subnet defined and source of the packet is
+          link-local. If this is the case server assumes that the only subnet
+          defined is local and client is indeed connected to it. This check
+          simplifies small deployments.
+        </para>
+        <para>If there are two or more subnets defined, server can not assume
+        which of those (if any) subnets are local. Therefore an optional
+        "interface" parameter has been defined to designate that a given subnet
+        is local, i.e. reachable directly over specified interface. For example
+        the server that is intended to serve local subnet over eth0 may be configured
+        as follows:
+        <screen>
+&gt; <userinput>config add Dhcp6/subnet6</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/subnet "2001:db8:beef::/48"</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/pool [ "2001:db8:beef::/48" ]</userinput>
+&gt; <userinput>config set Dhcp6/subnet6[1]/interface "eth0"</userinput>
+&gt; <userinput>config commit</userinput></screen>
+        </para>
+      </section>
     </section>
     </section>
 
 
     <section id="dhcp6-std">
     <section id="dhcp6-std">

+ 15 - 0
src/bin/dhcp6/config_parser.cc

@@ -1135,6 +1135,15 @@ private:
         Triplet<uint32_t> pref = getParam("preferred-lifetime");
         Triplet<uint32_t> pref = getParam("preferred-lifetime");
         Triplet<uint32_t> valid = getParam("valid-lifetime");
         Triplet<uint32_t> valid = getParam("valid-lifetime");
 
 
+        // Get interface name. If it is defined, then the subnet is available
+        // directly over specified network interface.
+
+        string iface;
+        StringStorage::iterator iface_iter = string_values_.find("interface");
+        if (iface_iter != string_values_.end()) {
+            iface = iface_iter->second;
+        }
+
         /// @todo: Convert this to logger once the parser is working reliably
         /// @todo: Convert this to logger once the parser is working reliably
         stringstream tmp;
         stringstream tmp;
         tmp << addr.toText() << "/" << (int)len
         tmp << addr.toText() << "/" << (int)len
@@ -1151,6 +1160,11 @@ private:
             subnet_->addPool(*it);
             subnet_->addPool(*it);
         }
         }
 
 
+        // Configure interface, if defined
+        if (iface.length()) {
+            subnet_->setIface(iface);
+        }
+
         Subnet::OptionContainerPtr options = subnet_->getOptionDescriptors("dhcp6");
         Subnet::OptionContainerPtr options = subnet_->getOptionDescriptors("dhcp6");
         const Subnet::OptionContainerTypeIndex& idx = options->get<1>();
         const Subnet::OptionContainerTypeIndex& idx = options->get<1>();
 
 
@@ -1205,6 +1219,7 @@ private:
         factories["subnet"] = StringParser::factory;
         factories["subnet"] = StringParser::factory;
         factories["pool"] = PoolParser::factory;
         factories["pool"] = PoolParser::factory;
         factories["option-data"] = OptionDataListParser::factory;
         factories["option-data"] = OptionDataListParser::factory;
+        factories["interface"] = StringParser::factory;
 
 
         FactoryMap::iterator f = factories.find(config_id);
         FactoryMap::iterator f = factories.find(config_id);
         if (f == factories.end()) {
         if (f == factories.end()) {

+ 6 - 0
src/bin/dhcp6/dhcp6.spec

@@ -94,6 +94,12 @@
                   "item_default": ""
                   "item_default": ""
                 },
                 },
 
 
+                { "item_name": "interface",
+                  "item_type": "string",
+                  "item_optional": false,
+                  "item_default": ""
+                },
+
                 { "item_name": "renew-timer",
                 { "item_name": "renew-timer",
                   "item_type": "integer",
                   "item_type": "integer",
                   "item_optional": false,
                   "item_optional": false,

+ 11 - 1
src/bin/dhcp6/dhcp6_srv.cc

@@ -428,7 +428,17 @@ void Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
 }
 }
 
 
 Subnet6Ptr Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
 Subnet6Ptr Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
-    Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
+
+    /// @todo: pass interface information only if received direct (non-relayed) message
+
+    // Try to find a subnet if received packet from a directly connected client
+    Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface());
+    if (subnet) {
+        return (subnet);
+    }
+
+    // If no subnet was found, try to find it based on remote address
+    subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
 
 
     return (subnet);
     return (subnet);
 }
 }

+ 60 - 4
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -272,9 +272,8 @@ TEST_F(Dhcp6ParserTest, bogusCommand) {
     EXPECT_EQ(1, rcode_);
     EXPECT_EQ(1, rcode_);
 }
 }
 
 
-/// The goal of this test is to verify if wrongly defined subnet will
-/// be rejected. Properly defined subnet must include at least one
-/// pool definition.
+/// The goal of this test is to verify if configuration without any
+/// subnets defined can be accepted.
 TEST_F(Dhcp6ParserTest, emptySubnet) {
 TEST_F(Dhcp6ParserTest, emptySubnet) {
 
 
     ConstElementPtr status;
     ConstElementPtr status;
@@ -365,6 +364,64 @@ TEST_F(Dhcp6ParserTest, subnetLocal) {
     EXPECT_EQ(4, subnet->getValid());
     EXPECT_EQ(4, subnet->getValid());
 }
 }
 
 
+// This test checks if it is possible to define a subnet with an
+// interface defined.
+TEST_F(Dhcp6ParserTest, subnetInterface) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interface\": [ \"all\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+        "    \"interface\": \"eth0\","
+        "    \"subnet\": \"2001:db8:1::/64\" } ],"
+        "\"valid-lifetime\": 4000 }";
+    cout << config << endl;
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+    // returned value should be 0 (configuration success)
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    EXPECT_EQ(0, rcode_);
+
+    Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
+    ASSERT_TRUE(subnet);
+    EXPECT_EQ("eth0", subnet->getIface());
+}
+
+// This test checks if it is not allowed to define global interface
+// parameter.
+TEST_F(Dhcp6ParserTest, interfaceGlobal) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interface\": [ \"all\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"interface\": \"eth0\"," // Not valid. Can be defined in subnet only
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+        "    \"subnet\": \"2001:db8:1::/64\" } ],"
+        "\"valid-lifetime\": 4000 }";
+    cout << config << endl;
+
+    ElementPtr json = Element::fromJSON(config);
+
+    EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+    // returned value should be 1 (parse error)
+    ASSERT_TRUE(status);
+    comment_ = parseAnswer(rcode_, status);
+    EXPECT_EQ(1, rcode_);
+}
+
 // Test verifies that a subnet with pool values that do not belong to that
 // Test verifies that a subnet with pool values that do not belong to that
 // pool are rejected.
 // pool are rejected.
 TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
 TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
@@ -389,7 +446,6 @@ TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
     // as the pool does not belong to that subnet
     // as the pool does not belong to that subnet
     ASSERT_TRUE(status);
     ASSERT_TRUE(status);
     comment_ = parseAnswer(rcode_, status);
     comment_ = parseAnswer(rcode_, status);
-
     EXPECT_EQ(1, rcode_);
     EXPECT_EQ(1, rcode_);
 }
 }
 
 

+ 114 - 0
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc

@@ -1294,6 +1294,120 @@ TEST_F(Dhcpv6SrvTest, sanityCheck) {
                  RFCViolation);
                  RFCViolation);
 }
 }
 
 
+// This test verifies if selectSubnet() selects proper subnet for a given
+// source address.
+TEST_F(Dhcpv6SrvTest, selectSubnetAddr) {
+    NakedDhcpv6Srv srv(0);
+
+    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+    // CASE 1: We have only one subnet defined and we received local traffic.
+    // The only available subnet should be selected
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+    Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    pkt->setRemoteAddr(IOAddress("fe80::abcd"));
+
+    Subnet6Ptr selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet1);
+
+    // CASE 2: We have only one subnet defined and we received relayed traffic.
+    // We should NOT select it.
+
+    // Identical steps as in case 1, but repeated for clarity
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+    pkt->setRemoteAddr(IOAddress("2001:db8:abcd::2345"));
+    selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+    // CASE 3: We have three subnets defined and we received local traffic.
+    // Nothing should be selected.
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+    pkt->setRemoteAddr(IOAddress("fe80::abcd"));
+    selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+    // CASE 4: We have three subnets defined and we received relayed traffic
+    // that came out of subnet 2. We should select subnet2 then
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+    pkt->setRemoteAddr(IOAddress("2001:db8:2::baca"));
+    selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet2);
+
+    // CASE 5: We have three subnets defined and we received relayed traffic
+    // that came out of undefined subnet. We should select nothing
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+    pkt->setRemoteAddr(IOAddress("2001:db8:4::baca"));
+    selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+}
+
+// This test verifies if selectSubnet() selects proper subnet for a given
+// network interface name.
+TEST_F(Dhcpv6SrvTest, selectSubnetIface) {
+    NakedDhcpv6Srv srv(0);
+
+    Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+    subnet1->setIface("eth0");
+    subnet3->setIface("wifi1");
+
+    // CASE 1: We have only one subnet defined and it is available via eth0.
+    // Packet came from eth0. The only available subnet should be selected
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+    Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    pkt->setIface("eth0");
+
+    Subnet6Ptr selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet1);
+
+    // CASE 2: We have only one subnet defined and it is available via eth0.
+    // Packet came from eth1. We should not select it
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+    pkt->setIface("eth1");
+
+    selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+    // CASE 3: We have only 3 subnets defined, one over eth0, one remote and
+    // one over wifi1.
+    // Packet came from eth1. We should not select it
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+
+    pkt->setIface("eth0");
+    EXPECT_EQ(subnet1, srv.selectSubnet(pkt));
+
+    pkt->setIface("eth3"); // no such interface
+    EXPECT_EQ(Subnet6Ptr(), srv.selectSubnet(pkt)); // nothing selected
+
+    pkt->setIface("wifi1");
+    EXPECT_EQ(subnet3, srv.selectSubnet(pkt));
+
+}
+
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
 /// to call processX() methods.
 /// to call processX() methods.
 
 

+ 21 - 0
src/lib/dhcpsrv/cfgmgr.cc

@@ -138,6 +138,26 @@ CfgMgr::getOptionDef(const std::string& option_space,
 }
 }
 
 
 Subnet6Ptr
 Subnet6Ptr
+CfgMgr::getSubnet6(const std::string& iface) {
+
+    if (!iface.length()) {
+        return (Subnet6Ptr());
+    }
+
+    // If there is more than one, we need to choose the proper one
+    for (Subnet6Collection::iterator subnet = subnets6_.begin();
+         subnet != subnets6_.end(); ++subnet) {
+        if (iface == (*subnet)->getIface()) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+                      DHCPSRV_CFGMGR_SUBNET6_IFACE)
+                .arg((*subnet)->toText()).arg(iface);
+            return (*subnet);
+        }
+    }
+    return (Subnet6Ptr());
+}
+
+Subnet6Ptr
 CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
 CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
 
 
     // If there's only one subnet configured, let's just use it
     // If there's only one subnet configured, let's just use it
@@ -158,6 +178,7 @@ CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
     // If there is more than one, we need to choose the proper one
     // If there is more than one, we need to choose the proper one
     for (Subnet6Collection::iterator subnet = subnets6_.begin();
     for (Subnet6Collection::iterator subnet = subnets6_.begin();
          subnet != subnets6_.end(); ++subnet) {
          subnet != subnets6_.end(); ++subnet) {
+
         if ((*subnet)->inRange(hint)) {
         if ((*subnet)->inRange(hint)) {
             LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
             LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
                       DHCPSRV_CFGMGR_SUBNET6)
                       DHCPSRV_CFGMGR_SUBNET6)

+ 10 - 1
src/lib/dhcpsrv/cfgmgr.h

@@ -153,9 +153,18 @@ public:
     ///
     ///
     /// @param hint an address that belongs to a searched subnet
     /// @param hint an address that belongs to a searched subnet
     ///
     ///
-    /// @return a subnet object
+    /// @return a subnet object (or NULL if no suitable match was fount)
     Subnet6Ptr getSubnet6(const isc::asiolink::IOAddress& hint);
     Subnet6Ptr getSubnet6(const isc::asiolink::IOAddress& hint);
 
 
+    /// @brief get IPv6 subnet by interface name
+    ///
+    /// Finds a matching local subnet, based on interface name. This
+    /// is used for selecting subnets that were explicitly marked by the
+    /// user as reachable over specified network interface.
+    /// @param iface_name interface name
+    /// @return a subnet object (or NULL if no suitable match was fount)
+    Subnet6Ptr getSubnet6(const std::string& iface_name);
+
     /// @brief get IPv6 subnet by interface-id
     /// @brief get IPv6 subnet by interface-id
     ///
     ///
     /// Another possibility to find a subnet is based on interface-id.
     /// Another possibility to find a subnet is based on interface-id.

+ 6 - 0
src/lib/dhcpsrv/dhcpsrv_messages.mes

@@ -60,6 +60,12 @@ This is a debug message reporting that the DHCP configuration manager has
 returned the specified IPv6 subnet when given the address hint specified
 returned the specified IPv6 subnet when given the address hint specified
 as the address is within the subnet.
 as the address is within the subnet.
 
 
+% DHCPSRV_CFGMGR_SUBNET6_IFACE selected subnet %1 for packet received over interface %2
+This is a debug message reporting that the DHCP configuration manager has
+returned the specified IPv6 subnet for a packet received over given interface.
+This particular subnet was selected, because it was specified as being directly
+reachable over given interface. (see 'interface' parameter in subnet6 definition).
+
 % DHCPSRV_INVALID_ACCESS invalid database access string: %1
 % DHCPSRV_INVALID_ACCESS invalid database access string: %1
 This is logged when an attempt has been made to parse a database access string
 This is logged when an attempt has been made to parse a database access string
 and the attempt ended in error.  The access string in question - which
 and the attempt ended in error.  The access string in question - which

+ 10 - 0
src/lib/dhcpsrv/subnet.cc

@@ -202,5 +202,15 @@ Subnet6::validateOption(const OptionPtr& option) const {
     }
     }
 }
 }
 
 
+
+void Subnet6::setIface(const std::string& iface_name) {
+    iface_ = iface_name;
+}
+
+std::string Subnet6::getIface() {
+    return (iface_);
+}
+
+
 } // end of isc::dhcp namespace
 } // end of isc::dhcp namespace
 } // end of isc namespace
 } // end of isc namespace

+ 14 - 0
src/lib/dhcpsrv/subnet.h

@@ -415,6 +415,9 @@ protected:
     /// fully trusted.
     /// fully trusted.
     isc::asiolink::IOAddress last_allocated_;
     isc::asiolink::IOAddress last_allocated_;
 
 
+    /// @brief Name of the network interface (if connected directly)
+    std::string iface_;
+
 private:
 private:
 
 
     /// Container holding options grouped by option space names.
     /// Container holding options grouped by option space names.
@@ -496,6 +499,17 @@ public:
         return (preferred_);
         return (preferred_);
     }
     }
 
 
+    /// @brief sets name of the network interface for directly attached networks
+    ///
+    /// A subnet may be reachable directly (not via relays). In DHCPv6 it is not
+    /// possible to decide that based on addresses assigned to network interfaces,
+    /// as DHCPv6 operates on link-local (and site local) addresses.
+    void setIface(const std::string& iface_name);
+
+    /// @brief network interface name used to reach subnet (or "" for remote subnets)
+    /// @return network interface name for directly attached subnets or ""
+    std::string getIface();
+
 protected:
 protected:
 
 
     /// @brief Check if option is valid and can be added to a subnet.
     /// @brief Check if option is valid and can be added to a subnet.

+ 4 - 0
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -351,4 +351,8 @@ TEST_F(CfgMgrTest, optionSpace6) {
     // @todo decide if a duplicate vendor space is allowed.
     // @todo decide if a duplicate vendor space is allowed.
 }
 }
 
 
+// No specific tests for getSubnet6. That method (2 overloaded versions) is tested
+// in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
+// (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)
+
 } // end of anonymous namespace
 } // end of anonymous namespace

+ 11 - 0
src/lib/dhcpsrv/tests/subnet_unittest.cc

@@ -505,4 +505,15 @@ TEST(Subnet6Test, get) {
     EXPECT_EQ(32, subnet.get().second);
     EXPECT_EQ(32, subnet.get().second);
 }
 }
 
 
+// This trivial test checks if interface name is stored properly
+// in Subnet6 objects.
+TEST(Subnet6Test, iface) {
+    Subnet6 subnet(IOAddress("2001:db8::"), 32, 1, 2, 3, 4);
+
+    EXPECT_EQ("", subnet.getIface());
+
+    subnet.setIface("en1");
+    EXPECT_EQ("en1", subnet.getIface());
+}
+
 };
 };