Browse Source

[2898] Support for v6 relayed traffic added.

Tomek Mrugalski 12 years ago
parent
commit
b82db9d608

+ 26 - 2
src/bin/dhcp6/config_parser.cc

@@ -1481,11 +1481,27 @@ private:
         std::string iface;
         try {
             iface = string_values_.getParam("interface");
-        } catch (DhcpConfigError) {
+        } catch (const DhcpConfigError&) {
             // iface not mandatory so swallow the exception
         }
 
-        /// @todo: Convert this to logger once the parser is working reliably
+        // Get interface-id option content. For now we support string
+        // represenation only
+        std::string ifaceid;
+        try {
+            ifaceid = string_values_.getParam("interface-id");
+        } catch (DhcpConfigError) {
+            // interface-id is not mandatory
+        }
+
+        if (!iface.empty() && !ifaceid.empty()) {
+            isc_throw(isc::dhcp::DhcpConfigError,
+                      "parser error: interface (defined for locally reachable "
+                      "subnets) and interface-id (defined for subnets reachable"
+                      " via relays) cannot be defined at the same time for "
+                      "subnet " << addr.toText() << "/" << (int)len);
+        }
+
         stringstream tmp;
         tmp << addr.toText() << "/" << (int)len
             << " with params t1=" << t1 << ", t2=" << t2 << ", pref="
@@ -1512,6 +1528,13 @@ private:
             subnet_->setIface(iface);
         }
 
+        // Configure interface-id for remote interfaces, if defined
+        if (!ifaceid.empty()) {
+            OptionBuffer tmp(ifaceid.begin(), ifaceid.end());
+            OptionPtr opt(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+            subnet_->setInterfaceId(opt);
+        }
+
         // We are going to move configured options to the Subnet object.
         // Configured options reside in the container where options
         // are grouped by space names. Thus we need to get all space names
@@ -1591,6 +1614,7 @@ private:
         factories["pool"] = PoolParser::factory;
         factories["option-data"] = OptionDataListParser::factory;
         factories["interface"] = StringParser::factory;
+        factories["interface-id"] = StringParser::factory;
 
         FactoryMap::iterator f = factories.find(config_id);
         if (f == factories.end()) {

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

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

+ 39 - 9
src/bin/dhcp6/dhcp6_srv.cc

@@ -403,8 +403,13 @@ Dhcpv6Srv::copyDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
     if (clientid) {
         answer->addOption(clientid);
     }
+    /// @todo: Should throw if there is no client-id (except anonymous INF-REQUEST)
+
+    // if this is a relayed message, we need to copy relay information
+    if (!question->relay_info_.empty()) {
+        answer->copyRelayInfo(question);
+    }
 
-    // TODO: Should throw if there is no client-id (except anonymous INF-REQUEST)
 }
 
 void
@@ -523,18 +528,43 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
 Subnet6Ptr
 Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
 
-    /// @todo: pass interface information only if received direct (non-relayed) message
+    Subnet6Ptr subnet;
 
-    // Try to find a subnet if received packet from a directly connected client
-    Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface());
-    if (subnet) {
+    if (question->relay_info_.empty()) {
+        // This is a 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);
-    }
+    } else {
 
-    // If no subnet was found, try to find it based on remote address
-    subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
+        // This is a relayed message
+        OptionPtr interface_id = question->getAnyRelayOption(D6O_INTERFACE_ID,
+                                                             Pkt6::RELAY_SEARCH_FIRST);
+        if (interface_id) {
+            subnet = CfgMgr::instance().getSubnet6(interface_id);
+        }
+
+        if (subnet) {
+            return (subnet);
+        }
+
+        // If no interface-id was specified (or not configured on server), let's
+        // try address matching
+        IOAddress link_addr = question->relay_info_.back().linkaddr_;
 
-    return (subnet);
+        // if relay filled in link_addr field, then let's use it
+        if (link_addr != IOAddress("::")) {
+            subnet = CfgMgr::instance().getSubnet6(link_addr);
+        }
+        return (subnet);
+    }
 }
 
 void

+ 104 - 0
src/bin/dhcp6/tests/config_parser_unittest.cc

@@ -500,6 +500,110 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
     EXPECT_EQ(1, rcode_);
 }
 
+
+// This test checks if it is possible to define a subnet with an
+// interface-id option defined.
+TEST_F(Dhcp6ParserTest, subnetInterfaceId) {
+
+    const string valid_interface_id = "foobar";
+    const string bogus_interface_id = "blah";
+
+    ConstElementPtr status;
+
+    // There should be at least one interface
+
+    string config = "{ "
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+        "    \"interface-id\": \"" + valid_interface_id + "\","
+        "    \"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_);
+
+    // try to get a subnet based on bogus interface-id option
+    OptionBuffer tmp(bogus_interface_id.begin(), bogus_interface_id.end());
+    OptionPtr ifaceid(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+    Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(ifaceid);
+    EXPECT_FALSE(subnet);
+
+    // now try to get subnet for valid interface-id value
+    tmp = OptionBuffer(valid_interface_id.begin(), valid_interface_id.end());
+    ifaceid.reset(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+    subnet = CfgMgr::instance().getSubnet6(ifaceid);
+    ASSERT_TRUE(subnet);
+    EXPECT_TRUE(ifaceid->equal(subnet->getInterfaceId()));
+}
+
+
+// This test checks if it is not allowed to define global interface
+// parameter.
+TEST_F(Dhcp6ParserTest, interfaceIdGlobal) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"interface\": [ \"all\" ],"
+        "\"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"interface-id\": \"foobar\"," // 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_);
+}
+
+// This test checks if it is not possible to define a subnet with an
+// interface (i.e. local subnet) and interface-id (remote subnet) defined.
+TEST_F(Dhcp6ParserTest, subnetInterfaceAndInterfaceId) {
+
+    ConstElementPtr status;
+
+    string config = "{ \"preferred-lifetime\": 3000,"
+        "\"rebind-timer\": 2000, "
+        "\"renew-timer\": 1000, "
+        "\"subnet6\": [ { "
+        "    \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+        "    \"interface\": \"" + valid_iface_ + "\","
+        "    \"interface-id\": \"foobar\","
+        "    \"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(1, rcode_);
+
+}
+
+
+
 // Test verifies that a subnet with pool values that do not belong to that
 // pool are rejected.
 TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {

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

@@ -98,6 +98,16 @@ public:
         return (ia);
     }
 
+    /// @brief generates interface-id option, based on text
+    ///
+    /// @param iface_id textual representation of the interface-id content
+    ///
+    /// @return pointer to the option object
+    OptionPtr generateInterfaceId(const string& iface_id) {
+        OptionBuffer tmp(iface_id.begin(), iface_id.end());
+        return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+    }
+
     // Generate client-id option
     OptionPtr generateClientId(size_t duid_size = 32) {
 
@@ -678,6 +688,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
     // check that IA_NA was returned and that there's an address included
     boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
                                                 subnet_->getT2());
+    ASSERT_TRUE(addr);
 
     // Check that the assigned address is indeed from the configured pool
     checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
@@ -731,6 +742,7 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
     // check that IA_NA was returned and that there's an address included
     boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
                                                 subnet_->getT2());
+    ASSERT_TRUE(addr);
 
     // check that we've got the address we requested
     checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
@@ -779,6 +791,7 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
     // check that IA_NA was returned and that there's an address included
     boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
                                                 subnet_->getT2());
+    ASSERT_TRUE(addr);
 
     // Check that the assigned address is indeed from the configured pool
     checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
@@ -840,6 +853,9 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
                                                 subnet_->getT2());
     boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(),
                                                 subnet_->getT2());
+    ASSERT_TRUE(addr1);
+    ASSERT_TRUE(addr2);
+    ASSERT_TRUE(addr3);
 
     // Check that the assigned address is indeed from the configured pool
     checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid());
@@ -910,6 +926,7 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
     // check that IA_NA was returned and that there's an address included
     boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
                                                 subnet_->getT2());
+    ASSERT_TRUE(addr);
 
     // check that we've got the address we requested
     checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
@@ -1592,6 +1609,113 @@ TEST_F(Dhcpv6SrvTest, selectSubnetIface) {
     EXPECT_EQ(subnet3, srv.selectSubnet(pkt));
 }
 
+// This test verifies if selectSubnet() selects proper subnet for a given
+// linkaddr in RELAY-FORW message
+TEST_F(Dhcpv6SrvTest, selectSubnetRelayLinkaddr) {
+    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));
+
+    Pkt6::RelayInfo relay;
+    relay.linkaddr_ = IOAddress("2001:db8:2::1234");
+    relay.peeraddr_ = IOAddress("fe80::1");
+
+    // CASE 1: We have only one subnet defined and we received relayed traffic.
+    // The only available subnet should NOT be selected.
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+    Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    pkt->relay_info_.push_back(relay);
+
+    Subnet6Ptr selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+    // CASE 2: We have three subnets defined and we received relayed traffic.
+    // Nothing should be selected.
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+    selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet2);
+
+    // CASE 3: 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);
+
+    // source of the packet should have no meaning. Selection is based
+    // on linkaddr field in the relay
+    pkt->setRemoteAddr(IOAddress("2001:db8:1::baca"));
+    selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet2);
+
+    // CASE 4: 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->relay_info_.clear();
+    relay.linkaddr_ = IOAddress("2001:db8:4::1234");
+    pkt->relay_info_.push_back(relay);
+    selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+}
+
+// This test verifies if selectSubnet() selects proper subnet for a given
+// interface-id option
+TEST_F(Dhcpv6SrvTest, selectSubnetRelayInterfaceId) {
+    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->setInterfaceId(generateInterfaceId("relay1"));
+    subnet2->setInterfaceId(generateInterfaceId("relay2"));
+
+    // CASE 1: We have only one subnet defined and it is for interface-id "relay1"
+    // Packet came with interface-id "relay2". We should not select subnet1
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+    Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+    Pkt6::RelayInfo relay;
+    relay.linkaddr_ = IOAddress("2001:db8:2::1234");
+    relay.peeraddr_ = IOAddress("fe80::1");
+    OptionPtr opt = generateInterfaceId("relay2");
+    relay.options_.insert(pair<int, OptionPtr>(opt->getType(), opt));
+    pkt->relay_info_.push_back(relay);
+
+    // There is only one subnet configured and we are outside of that subnet
+    Subnet6Ptr selected = srv.selectSubnet(pkt);
+    EXPECT_FALSE(selected);
+
+    // CASE 2: We have only one subnet defined and it is for interface-id "relay2"
+    // Packet came with interface-id "relay2". We should select it
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet2); // just a single subnet
+    selected = srv.selectSubnet(pkt);
+    EXPECT_EQ(selected, subnet2);
+
+    // CASE 3: We have only 3 subnets defined: one remote for interface-id "relay1",
+    // one remote for interface-id "relay2" and third local
+    // packet comes with interface-id "relay2". We should select subnet2
+    CfgMgr::instance().deleteSubnets6();
+    CfgMgr::instance().addSubnet6(subnet1);
+    CfgMgr::instance().addSubnet6(subnet2);
+    CfgMgr::instance().addSubnet6(subnet3);
+
+    EXPECT_EQ(subnet2, srv.selectSubnet(pkt));
+}
+
 // This test verifies if the server-id disk operations (read, write) are
 // working properly.
 TEST_F(Dhcpv6SrvTest, ServerID) {

+ 81 - 0
src/lib/dhcp/pkt6.cc

@@ -72,6 +72,60 @@ uint16_t Pkt6::len() {
     }
 }
 
+OptionPtr Pkt6::getAnyRelayOption(uint16_t opt_type, RelaySearchOrder order) {
+    int start = 0;
+    int end = 0;
+    int direction = 0;
+
+    if (relay_info_.empty()) {
+        // there's no relay info, this is a direct message
+        return (OptionPtr());
+    }
+
+    switch (order) {
+    case RELAY_SEARCH_FROM_CLIENT:
+        // search backwards
+        start = relay_info_.size() - 1;
+        end = 0;
+        direction = -1;
+        break;
+    case RELAY_SEARCH_FROM_SERVER:
+        // search forward
+        start = 0;
+        end = relay_info_.size() - 1;
+        direction = 1;
+        break;
+    case RELAY_SEARCH_FIRST:
+        // look at the innermost relay only
+        start = relay_info_.size() - 1;
+        end = start;
+        direction = 0;
+        break;
+    case RELAY_SEARCH_LAST:
+        // look at the outermost relay only
+        start = 0;
+        end = 0;
+        direction = 0;
+    }
+
+    // this is a tricky loop. It must go from start to end, but it must work in
+    // both directions (start > end; or start < end). We can't use regular
+    // exit condition, because we don't know whether to use i <= end or i >= end
+    for (int i = start; ; i += direction) {
+        OptionPtr opt = getRelayOption(opt_type, i);
+        if (opt) {
+            return (opt);
+        }
+
+        if (i == end) {
+            break;
+        }
+    }
+
+    return getRelayOption(opt_type, end);
+}
+
+
 OptionPtr Pkt6::getRelayOption(uint16_t opt_type, uint8_t relay_level) {
     if (relay_level >= relay_info_.size()) {
         isc_throw(OutOfRange, "This message was relayed " << relay_info_.size() << " time(s)."
@@ -483,5 +537,32 @@ const char* Pkt6::getName() const {
     return (getName(getType()));
 }
 
+void Pkt6::copyRelayInfo(const Pkt6Ptr& question) {
+
+    // we use index rather than iterator, because we need that as a parameter
+    // passed to getRelayOption()
+    for (int i = 0; i < question->relay_info_.size(); ++i) {
+        RelayInfo x;
+        x.msg_type_ = DHCPV6_RELAY_REPL;
+        x.hop_count_ = question->relay_info_[i].hop_count_;
+        x.linkaddr_ = question->relay_info_[i].linkaddr_;
+        x.peeraddr_ = question->relay_info_[i].peeraddr_;
+
+        // Is there interface-id option in this nesting level if there is,
+        // we need to echo it back
+        OptionPtr opt = question->getRelayOption(D6O_INTERFACE_ID, i);
+        // taken from question->RelayInfo_[i].options_
+        if (opt) {
+            x.options_.insert(pair<int, boost::shared_ptr<Option> >(opt->getType(), opt));
+        }
+
+        /// @todo: implement support for ERO (Echo Request Option, RFC4994)
+
+        // add this relay-repl info to our message
+        relay_info_.push_back(x);
+    }
+}
+
+
 } // end of isc::dhcp namespace
 } // end of isc namespace

+ 45 - 2
src/lib/dhcp/pkt6.h

@@ -30,6 +30,9 @@ namespace isc {
 
 namespace dhcp {
 
+class Pkt6;
+typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
+
 class Pkt6 {
 public:
     /// specifies non-relayed DHCPv6 packet header length (over UDP)
@@ -44,6 +47,28 @@ public:
         TCP = 1  // there are TCP DHCPv6 packets (bulk leasequery, failover)
     };
 
+    /// @brief defines relay search pattern
+    ///
+    /// Defines order in which options are searched in a message that
+    /// passed through mulitple relays. RELAY_SEACH_FROM_CLIENT will
+    /// start search from the relay that was the closest to the client
+    /// (i.e. innermost in the encapsulated message, which also means
+    /// this was the first relay that forwarded packet received by the
+    /// server and this will be the last relay that will handle the
+    /// response that server sent towards the client.).
+    /// RELAY_SEARCH_FROM_SERVER is the opposite. This will be the
+    /// relay closest to the server (i.e. outermost in the encapsulated
+    /// message, which also means it was the last relay that relayed
+    /// the received message and will be the first one to process
+    /// server's response). RELAY_SEARCH_FIRST is option from the first
+    /// relay (closest to the client), RELAY_SEARCH_LAST is the
+    /// last relay (closest to the server).
+    enum RelaySearchOrder {
+        RELAY_SEARCH_FROM_CLIENT = 1,
+        RELAY_SEARCH_FROM_SERVER = 2,
+        RELAY_SEARCH_FIRST = 3,
+        RELAY_SEARCH_LAST = 4
+    };
 
     /// @brief structure that describes a single relay information
     ///
@@ -201,6 +226,18 @@ public:
     /// @return pointer to the option (or NULL if there is no such option)
     OptionPtr getRelayOption(uint16_t option_code, uint8_t nesting_level);
 
+    /// @brief returns first instance of a specified option
+    ///
+    /// When client's packet traverses multiple relays, each passing relay
+    /// may insert extra options. This method allows getting specific instance
+    /// of a given option (closest to the client, closest to the server, etc.)
+    /// See @ref RelaySearchOrder for detailed description.
+    ///
+    /// @param option_code searched option
+    /// @param order option search order (see @ref RelaySearchOrder)
+    /// @return option pointer (or NULL if no option matches specified criteria)
+    OptionPtr getAnyRelayOption(uint16_t option_code, RelaySearchOrder order);
+
     /// @brief Returns all instances of specified type.
     ///
     /// Returns all instances of options of the specified type. DHCPv6 protocol
@@ -356,6 +393,14 @@ public:
     ///         be freed by the caller.
     const char* getName() const;
 
+    /// @brief copies relay information from client's packet to server's response
+    ///
+    /// This information is not simply copied over. Some parameter are
+    /// removed, msg_type_is updated (RELAY-FORW => RELAY-REPL), etc.
+    ///
+    /// @param question client's packet
+    void copyRelayInfo(const Pkt6Ptr& question);
+
     /// relay information
     ///
     /// this is a public field. Otherwise we hit one of the two problems:
@@ -494,8 +539,6 @@ protected:
     boost::posix_time::ptime timestamp_;
 }; // Pkt6 class
 
-typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
-
 } // isc::dhcp namespace
 
 } // isc namespace

+ 103 - 0
src/lib/dhcp/tests/pkt6_unittest.cc

@@ -22,6 +22,7 @@
 #include <dhcp/option_int.h>
 #include <dhcp/option_int_array.h>
 #include <dhcp/pkt6.h>
+#include <util/range_utilities.h>
 
 #include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/scoped_ptr.hpp>
@@ -44,6 +45,18 @@ class Pkt6Test : public ::testing::Test {
 public:
     Pkt6Test() {
     }
+
+    /// @brief generates an option with given code (and length) and random content
+    ///
+    /// @param code option code
+    /// @param len data length (data will be randomized)
+    ///
+    /// @return pointer to the new option
+    OptionPtr generateRandomOption(uint16_t code, size_t len = 10) {
+        OptionBuffer data(len);
+        util::fillRandom(data.begin(), data.end());
+        return OptionPtr(new Option(Option::V6, code, data));
+    }
 };
 
 TEST_F(Pkt6Test, constructor) {
@@ -552,4 +565,94 @@ TEST_F(Pkt6Test, relayPack) {
     EXPECT_EQ(0, memcmp(relay_opt_data, relay_opt_data, sizeof(relay_opt_data)));
 }
 
+
+// This test verified that options added by relays to the message can be
+// accessed and retrieved properly
+TEST_F(Pkt6Test, getAnyRelayOption) {
+
+    boost::scoped_ptr<Pkt6> msg(new Pkt6(DHCPV6_ADVERTISE, 0x020304));
+    msg->addOption(generateRandomOption(300));
+
+    // generate options for relay1
+    Pkt6::RelayInfo relay1;
+
+    // generate 3 options with code 200,201,202 and random content
+    OptionPtr relay1_opt1(generateRandomOption(200));
+    OptionPtr relay1_opt2(generateRandomOption(201));
+    OptionPtr relay1_opt3(generateRandomOption(202));
+
+    relay1.options_.insert(pair<int, boost::shared_ptr<Option> >(200, relay1_opt1));
+    relay1.options_.insert(pair<int, boost::shared_ptr<Option> >(201, relay1_opt2));
+    relay1.options_.insert(pair<int, boost::shared_ptr<Option> >(202, relay1_opt3));
+    msg->addRelayInfo(relay1);
+
+    // generate options for relay2
+    Pkt6::RelayInfo relay2;
+    OptionPtr relay2_opt1(new Option(Option::V6, 100));
+    OptionPtr relay2_opt2(new Option(Option::V6, 101));
+    OptionPtr relay2_opt3(new Option(Option::V6, 102));
+    OptionPtr relay2_opt4(new Option(Option::V6, 200)); // the same code as relay1_opt3
+    relay2.options_.insert(pair<int, boost::shared_ptr<Option> >(100, relay2_opt1));
+    relay2.options_.insert(pair<int, boost::shared_ptr<Option> >(101, relay2_opt2));
+    relay2.options_.insert(pair<int, boost::shared_ptr<Option> >(102, relay2_opt3));
+    relay2.options_.insert(pair<int, boost::shared_ptr<Option> >(200, relay2_opt4));
+    msg->addRelayInfo(relay2);
+
+    // generate options for relay3
+    Pkt6::RelayInfo relay3;
+    OptionPtr relay3_opt1(generateRandomOption(200, 7));
+    relay3.options_.insert(pair<int, boost::shared_ptr<Option> >(200, relay3_opt1));
+    msg->addRelayInfo(relay3);
+
+    // Ok, so we now have a packet that traversed the following network:
+    // client---relay3---relay2---relay1---server
+
+    // First check that the getAnyRelayOption does not confuse client options
+    // and relay options
+    OptionPtr opt = msg->getAnyRelayOption(300, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+    // 300 is a client option, present in the message itself.
+    EXPECT_FALSE(opt);
+
+    // Option 200 is added in every relay.
+
+    // We want to get that one inserted by relay3 (first match, starting from
+    // closest to the client.
+    opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay3_opt1));
+
+    // We want to ge that one inserted by relay1 (first match, starting from
+    // closest to the server.
+    opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_FROM_SERVER);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay1_opt1));
+
+    // We just want option from the first relay (closest to the client)
+    opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_FIRST);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay3_opt1));
+
+    // We just want option from the last relay (closest to the server)
+    opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_LAST);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay1_opt1));
+
+    // Let's try to ask for something that is inserted by the middle relay
+    // only.
+    opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_FROM_SERVER);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay2_opt1));
+
+    opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+    ASSERT_TRUE(opt);
+    EXPECT_TRUE(opt->equal(relay2_opt1));
+
+    opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_FIRST);
+    EXPECT_FALSE(opt);
+
+    opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_LAST);
+    EXPECT_FALSE(opt);
+
+}
+
 }

+ 18 - 3
src/lib/dhcpsrv/cfgmgr.cc

@@ -178,14 +178,29 @@ CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
     return (Subnet6Ptr());
 }
 
-Subnet6Ptr CfgMgr::getSubnet6(OptionPtr /*interfaceId*/) {
-    /// @todo: Implement get subnet6 by interface-id (for relayed traffic)
-    isc_throw(NotImplemented, "Relayed DHCPv6 traffic is not supported yet.");
+Subnet6Ptr CfgMgr::getSubnet6(OptionPtr iface_id_option) {
+    if (!iface_id_option) {
+        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 ( (*subnet)->getInterfaceId() &&
+             ((*subnet)->getInterfaceId()->equal(iface_id_option))) {
+            LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+                      DHCPSRV_CFGMGR_SUBNET6_IFACE_ID)
+                .arg((*subnet)->toText());
+            return (*subnet);
+        }
+    }
+    return (Subnet6Ptr());
 }
 
 void CfgMgr::addSubnet6(const Subnet6Ptr& subnet) {
     /// @todo: Check that this new subnet does not cross boundaries of any
     /// other already defined subnet.
+    /// @todo: Check that there is no subnet with the same interface-id
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_SUBNET6)
               .arg(subnet->toText());
     subnets6_.push_back(subnet);

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

@@ -174,7 +174,6 @@ public:
     /// @param interface_id content of interface-id option returned by a relay
     ///
     /// @return a subnet object
-    /// @todo This method is not currently supported.
     Subnet6Ptr getSubnet6(OptionPtr interface_id);
 
     /// @brief adds an IPv6 subnet

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

@@ -107,6 +107,13 @@ 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_CFGMGR_SUBNET6_IFACE_ID selected subnet %1 (interface-id match) for incoming packet
+This is a debug message reporting that the DHCP configuration manager
+has returned the specified IPv6 subnet for a received packet. This particular
+subnet was selected, because value of interface-id option matched what was
+configured in server's interface-id option for that selected subnet6.
+(see 'interface-id' parameter in subnet6 definition).
+
 % DHCPSRV_CLOSE_DB closing currently open %1 database
 This is a debug message, issued when the DHCP server closes the currently
 open lease database.  It is issued at program shutdown and whenever

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

@@ -463,6 +463,19 @@ public:
     /// @return network interface name for directly attached subnets or ""
     std::string getIface() const;
 
+    /// @brief sets interface-id option (if defined)
+    ///
+    /// @param ifaceid pointer to interface-id option
+    void setInterfaceId(const OptionPtr& ifaceid) {
+        interface_id_ = ifaceid;
+    }
+
+    /// @brief returns interface-id value (if specified)
+    /// @return interface-id option (if defined)
+    OptionPtr getInterfaceId() const {
+        return interface_id_;
+    }
+
 protected:
 
     /// @brief Check if option is valid and can be added to a subnet.
@@ -478,6 +491,9 @@ protected:
         return (isc::asiolink::IOAddress("::"));
     }
 
+    /// @brief specifies optional interface-id
+    OptionPtr interface_id_;
+
     /// @brief collection of pools in that list
     Pool6Collection pools_;
 

+ 106 - 10
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc

@@ -17,6 +17,7 @@
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/dhcp_config_parser.h>
 #include <exceptions/exceptions.h>
+#include <dhcp/dhcp6.h>
 
 #include <gtest/gtest.h>
 
@@ -37,7 +38,7 @@ using boost::scoped_ptr;
 
 namespace {
 
-// This test verifies that BooleanStorage functions properly. 
+// This test verifies that BooleanStorage functions properly.
 TEST(ValueStorageTest, BooleanTesting) {
     BooleanStorage testStore;
 
@@ -48,7 +49,7 @@ TEST(ValueStorageTest, BooleanTesting) {
     EXPECT_FALSE(testStore.getParam("firstBool"));
     EXPECT_TRUE(testStore.getParam("secondBool"));
 
-    // Verify that we can update paramaters. 
+    // Verify that we can update paramaters.
     testStore.setParam("firstBool", true);
     testStore.setParam("secondBool", false);
 
@@ -65,7 +66,7 @@ TEST(ValueStorageTest, BooleanTesting) {
     // Verify that looking for a parameter that never existed throws.
     ASSERT_THROW(testStore.getParam("bogusBool"), isc::dhcp::DhcpConfigError);
 
-    // Verify that attempting to delete a parameter that never existed does not throw. 
+    // Verify that attempting to delete a parameter that never existed does not throw.
     EXPECT_NO_THROW(testStore.delParam("bogusBool"));
 
     // Verify that we can empty the list.
@@ -74,21 +75,21 @@ TEST(ValueStorageTest, BooleanTesting) {
 
 }
 
-// This test verifies that Uint32Storage functions properly. 
+// This test verifies that Uint32Storage functions properly.
 TEST(ValueStorageTest, Uint32Testing) {
     Uint32Storage testStore;
 
     uint32_t intOne = 77;
     uint32_t intTwo = 33;
 
-    // Verify that we can add and retrieve parameters. 
+    // Verify that we can add and retrieve parameters.
     testStore.setParam("firstInt", intOne);
     testStore.setParam("secondInt", intTwo);
 
     EXPECT_EQ(testStore.getParam("firstInt"), intOne);
     EXPECT_EQ(testStore.getParam("secondInt"), intTwo);
 
-    // Verify that we can update parameters. 
+    // Verify that we can update parameters.
     testStore.setParam("firstInt", --intOne);
     testStore.setParam("secondInt", ++intTwo);
 
@@ -105,7 +106,7 @@ TEST(ValueStorageTest, Uint32Testing) {
     // Verify that looking for a parameter that never existed throws.
     ASSERT_THROW(testStore.getParam("bogusInt"), isc::dhcp::DhcpConfigError);
 
-    // Verify that attempting to delete a parameter that never existed does not throw. 
+    // Verify that attempting to delete a parameter that never existed does not throw.
     EXPECT_NO_THROW(testStore.delParam("bogusInt"));
 
     // Verify that we can empty the list.
@@ -113,7 +114,7 @@ TEST(ValueStorageTest, Uint32Testing) {
     EXPECT_THROW(testStore.getParam("secondInt"), isc::dhcp::DhcpConfigError);
 }
 
-// This test verifies that StringStorage functions properly. 
+// This test verifies that StringStorage functions properly.
 TEST(ValueStorageTest, StringTesting) {
     StringStorage testStore;
 
@@ -127,7 +128,7 @@ TEST(ValueStorageTest, StringTesting) {
     EXPECT_EQ(testStore.getParam("firstString"), stringOne);
     EXPECT_EQ(testStore.getParam("secondString"), stringTwo);
 
-    // Verify that we can update parameters. 
+    // Verify that we can update parameters.
     stringOne.append("-boo");
     stringTwo.append("-boo");
 
@@ -147,7 +148,7 @@ TEST(ValueStorageTest, StringTesting) {
     // Verify that looking for a parameter that never existed throws.
     ASSERT_THROW(testStore.getParam("bogusString"), isc::dhcp::DhcpConfigError);
 
-    // Verify that attempting to delete a parameter that never existed does not throw. 
+    // Verify that attempting to delete a parameter that never existed does not throw.
     EXPECT_NO_THROW(testStore.delParam("bogusString"));
 
     // Verify that we can empty the list.
@@ -165,6 +166,16 @@ public:
         CfgMgr::instance().deleteSubnets6();
     }
 
+    /// @brief generates interface-id option based on provided text
+    ///
+    /// @param text content of the option to be created
+    ///
+    /// @return pointer to the option object created
+    OptionPtr generateInterfaceId(const string& text) {
+        OptionBuffer buffer(text.begin(), text.end());
+        return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, buffer));
+    }
+
     ~CfgMgrTest() {
         // clean up after the test
         CfgMgr::instance().deleteSubnets4();
@@ -406,6 +417,91 @@ TEST_F(CfgMgrTest, subnet6) {
     EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("4000::123")));
 }
 
+// This test verifies if the configuration manager is able to hold, select
+// and return valid subnets, based on interface names.
+TEST_F(CfgMgrTest, subnet6Interface) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+    subnet1->setIface("foo");
+    subnet2->setIface("bar");
+    subnet3->setIface("foobar");
+
+    // There shouldn't be any subnet configured at this stage
+    EXPECT_FALSE(cfg_mgr.getSubnet6("foo"));
+
+    cfg_mgr.addSubnet6(subnet1);
+
+    // Now we have only one subnet, any request will be served from it
+    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6("foo"));
+
+    // If we have only a single subnet and the request came from a local
+    // address, let's use that subnet
+    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef")));
+
+    cfg_mgr.addSubnet6(subnet2);
+    cfg_mgr.addSubnet6(subnet3);
+
+    EXPECT_EQ(subnet3, cfg_mgr.getSubnet6("foobar"));
+    EXPECT_EQ(subnet2, cfg_mgr.getSubnet6("bar"));
+    EXPECT_FALSE(cfg_mgr.getSubnet6("xyzzy")); // no such interface
+
+    // Check that deletion of the subnets works.
+    cfg_mgr.deleteSubnets6();
+    EXPECT_FALSE(cfg_mgr.getSubnet6("foo"));
+    EXPECT_FALSE(cfg_mgr.getSubnet6("bar"));
+    EXPECT_FALSE(cfg_mgr.getSubnet6("foobar"));
+}
+
+// This test verifies if the configuration manager is able to hold, select
+// and return valid leases, based on interface-id option values
+TEST_F(CfgMgrTest, subnet6InterfaceId) {
+    CfgMgr& cfg_mgr = CfgMgr::instance();
+
+    Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+    Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+
+    // interface-id options used in subnets 1,2, and 3
+    OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
+    OptionPtr ifaceid2 = generateInterfaceId("VL32");
+    // That's a strange interface-id, but this is a real life example
+    OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");
+
+    // bogus interface-id
+    OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");
+
+    subnet1->setInterfaceId(ifaceid1);
+    subnet2->setInterfaceId(ifaceid2);
+    subnet3->setInterfaceId(ifaceid3);
+
+    // There shouldn't be any subnet configured at this stage
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid1));
+
+    cfg_mgr.addSubnet6(subnet1);
+
+    // If we have only a single subnet and the request came from a local
+    // address, let's use that subnet
+    EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(ifaceid1));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid2));
+
+    cfg_mgr.addSubnet6(subnet2);
+    cfg_mgr.addSubnet6(subnet3);
+
+    EXPECT_EQ(subnet3, cfg_mgr.getSubnet6(ifaceid3));
+    EXPECT_EQ(subnet2, cfg_mgr.getSubnet6(ifaceid2));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid_bogus));
+
+    // Check that deletion of the subnets works.
+    cfg_mgr.deleteSubnets6();
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid1));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid2));
+    EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid3));
+}
+
+
 // This test verifies that new DHCPv4 option spaces can be added to
 // the configuration manager and that duplicated option space is
 // rejected.

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

@@ -16,6 +16,7 @@
 
 #include <asiolink/io_address.h>
 #include <dhcp/option.h>
+#include <dhcp/dhcp6.h>
 #include <dhcpsrv/subnet.h>
 #include <exceptions/exceptions.h>
 
@@ -516,4 +517,19 @@ TEST(Subnet6Test, iface) {
     EXPECT_EQ("en1", subnet.getIface());
 }
 
+// This trivial test checks if the interface-id option can be set and
+// later retrieved for a subnet6 object.
+TEST(Subnet6Test, interfaceId) {
+    // Create as subnet to add options to it.
+    Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+    EXPECT_FALSE(subnet->getInterfaceId());
+
+    OptionPtr option(new Option(Option::V6, D6O_INTERFACE_ID, OptionBuffer(10, 0xFF)));
+    subnet->setInterfaceId(option);
+
+    EXPECT_EQ(option, subnet->getInterfaceId());
+
+}
+
 };