Parcourir la source

[master] Merge branch 'trac5377'

Marcin Siodelski il y a 7 ans
Parent
commit
d08088923e

+ 8 - 0
doc/examples/kea4/advanced.json

@@ -31,6 +31,14 @@
         // Ethernet/IP stack processing.
         "dhcp-socket-type": "udp",
 
+        // Typically the DHCP server will send its response back on the same
+        // interface the query came in. This is the default ("same-as-inbound").
+        // However, sometimes it is useful to have the ability to send the
+        // packet as plain UDP packet and let the kernel and the routing tables
+        // determine the right interface ("use-routing"). This option  only works
+        // for "dhcp-socket-type" set to "udp" and is ignored otherwise.
+        "outbound-interface": "use-routing",
+
         // This makes interfaces to be re-detected at each (re-)configuration.
         // By default it is true.
         "re-detect": true

+ 25 - 0
doc/guide/dhcp4-srv.xml

@@ -720,6 +720,31 @@ temporarily override a list of interface names and listen on all interfaces.
     fall back to use IP/UDP sockets.</para>
   </note>
 
+  <para>In typical environment the DHCP server is expected to send back a
+  response on the same network interface on which the query is received. This is
+  the default behavior. However, in some deployments it is desired that the
+  outbound (response) packets will be sent as regular traffic and the outbound
+  interface will be determined by the routing tables. This kind of asymetric
+  traffic is uncommon, but valid. Kea now supports a parameter called
+  <command>outbound-interface</command> that controls this behavior. It supports
+  two values. The first one, <userinput>same-as-inbound</userinput>, tells Kea
+  to send back the response on the same inteface the query packet is received. This
+  is the default behavior. The second one, <userinput>use-routing</userinput>
+  tells Kea to send regular UDP packets and let the kernel's routing table to
+  determine most appropriate interface. This only works when
+  <command>dhcp-socket-type</command> is  set to <userinput>udp</userinput>.
+  An example configuration looks as follows:
+  <screen>
+"Dhcp4": {
+    "interfaces-config": {
+        "interfaces": [ "eth1", "eth3" ],
+        "dhcp-socket-type": "udp",
+        <userinput>"outbound-interface": "use-routing"</userinput>
+    },
+    ...
+}</screen>
+  </para>
+
   <para>Interfaces are re-detected at each reconfiguration. This behavior
   can be disabled by setting <command>re-detect</command> value to
   <userinput>false</userinput>, for instance:

+ 27 - 0
src/bin/dhcp4/dhcp4_lexer.ll

@@ -229,6 +229,33 @@ ControlCharacterFill            [^"\\]|\\{JSONEscapeSequence}
     }
 }
 
+\"outbound-interface\" {
+    switch(driver.ctx_) {
+    case Parser4Context::INTERFACES_CONFIG:
+        return  isc::dhcp::Dhcp4Parser::make_OUTBOUND_INTERFACE(driver.loc_);
+    default:
+        return isc::dhcp::Dhcp4Parser::make_STRING("outbound-interface", driver.loc_);
+    }
+}
+
+\"same-as-inbound\" {
+    switch(driver.ctx_) {
+    case Parser4Context::OUTBOUND_INTERFACE:
+        return Dhcp4Parser::make_SAME_AS_INBOUND(driver.loc_);
+    default:
+        return Dhcp4Parser::make_STRING("same-as-inbound", driver.loc_);
+    }
+}
+
+\"use-routing\" {
+    switch(driver.ctx_) {
+    case Parser4Context::OUTBOUND_INTERFACE:
+        return Dhcp4Parser::make_USE_ROUTING(driver.loc_);
+    default:
+        return Dhcp4Parser::make_STRING("use-routing", driver.loc_);
+    }
+}
+
 \"interfaces\" {
     switch(driver.ctx_) {
     case isc::dhcp::Parser4Context::INTERFACES_CONFIG:

+ 18 - 0
src/bin/dhcp4/dhcp4_parser.yy

@@ -55,6 +55,9 @@ using namespace std;
   DHCP_SOCKET_TYPE "dhcp-socket-type"
   RAW "raw"
   UDP "udp"
+  OUTBOUND_INTERFACE "outbound-interface"
+  SAME_AS_INBOUND "same-as-inbound"
+  USE_ROUTING "use-routing"
   RE_DETECT "re-detect"
 
   ECHO_CLIENT_ID "echo-client-id"
@@ -212,6 +215,7 @@ using namespace std;
 %type <ElementPtr> value
 %type <ElementPtr> map_value
 %type <ElementPtr> socket_type
+%type <ElementPtr> outbound_interface_value
 %type <ElementPtr> db_type
 %type <ElementPtr> hr_mode
 %type <ElementPtr> ncr_protocol_value
@@ -477,6 +481,7 @@ interfaces_config_params: interfaces_config_param
 
 interfaces_config_param: interfaces_list
                        | dhcp_socket_type
+                       | outbound_interface
                        | re_detect
                        ;
 
@@ -510,6 +515,19 @@ socket_type: RAW { $$ = ElementPtr(new StringElement("raw", ctx.loc2pos(@1))); }
            | UDP { $$ = ElementPtr(new StringElement("udp", ctx.loc2pos(@1))); }
            ;
 
+outbound_interface: OUTBOUND_INTERFACE {
+    ctx.enter(ctx.OUTBOUND_INTERFACE);
+} COLON outbound_interface_value {
+    ctx.stack_.back()->set("outbound-interface", $4);
+    ctx.leave();
+}
+
+outbound_interface_value: SAME_AS_INBOUND {
+    $$ = ElementPtr(new StringElement("same-as-inbound", ctx.loc2pos(@1)));
+} | USE_ROUTING {
+    $$ = ElementPtr(new StringElement("use-routing", ctx.loc2pos(@1)));
+    } ;
+
 re_detect: RE_DETECT COLON BOOLEAN {
     ElementPtr b(new BoolElement($3, ctx.loc2pos(@3)));
     ctx.stack_.back()->set("re-detect", b);

+ 60 - 37
src/bin/dhcp4/dhcp4_srv.cc

@@ -26,6 +26,7 @@
 #include <dhcpsrv/callout_handle_store.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/cfg_host_operations.h>
+#include <dhcpsrv/cfg_iface.h>
 #include <dhcpsrv/cfg_subnets4.h>
 #include <dhcpsrv/lease_mgr.h>
 #include <dhcpsrv/lease_mgr_factory.h>
@@ -846,11 +847,12 @@ Dhcpv4Srv::run_one() {
             .arg(rsp->getLabel())
             .arg(rsp->getName())
             .arg(static_cast<int>(rsp->getType()))
-            .arg(rsp->getLocalAddr())
+            .arg(rsp->getLocalAddr().isV4Zero() ? "*" : rsp->getLocalAddr().toText())
             .arg(rsp->getLocalPort())
             .arg(rsp->getRemoteAddr())
             .arg(rsp->getRemotePort())
-            .arg(rsp->getIface());
+            .arg(rsp->getIface().empty() ? "to be determined from routing" :
+                 rsp->getIface());
 
         LOG_DEBUG(packet4_logger, DBG_DHCP4_DETAIL_DATA,
                   DHCP4_RESPONSE_DATA)
@@ -1139,13 +1141,20 @@ Dhcpv4Srv::srvidToString(const OptionPtr& srvid) {
 
 void
 Dhcpv4Srv::appendServerID(Dhcpv4Exchange& ex) {
-    // The source address for the outbound message should have been set already.
-    // This is the address that to the best of the server's knowledge will be
-    // available from the client.
-    /// @todo: perhaps we should consider some more sophisticated server id
-    /// generation, but for the current use cases, it should be ok.
+
+    // Use local address on which the packet has been received as a
+    // server identifier. In some cases it may be a different address,
+    // e.g. broadcast packet or DHCPv4o6 packet.
+    IOAddress local_addr = ex.getQuery()->getLocalAddr();
+    Pkt4Ptr query = ex.getQuery();
+
+    if (local_addr.isV4Bcast() || query->isDhcp4o6()) {
+        SocketInfo sock_info = IfaceMgr::instance().getSocket(*query);
+        local_addr = sock_info.addr_;
+    }
+
     OptionPtr opt_srvid(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
-                                           ex.getResponse()->getLocalAddr()));
+                                           local_addr));
     ex.getResponse()->addOption(opt_srvid);
 }
 
@@ -2027,38 +2036,52 @@ Dhcpv4Srv::adjustIfaceData(Dhcpv4Exchange& ex) {
         response->setRemotePort(DHCP4_SERVER_PORT);
     }
 
-    IOAddress local_addr = query->getLocalAddr();
+    CfgIfacePtr cfg_iface = CfgMgr::instance().getCurrentCfg()->getCfgIface();
+    if (query->isRelayed() &&
+        (cfg_iface->getSocketType() == CfgIface::SOCKET_UDP) &&
+        (cfg_iface->getOutboundIface() == CfgIface::USE_ROUTING)) {
 
-    // In many cases the query is sent to a broadcast address. This address
-    // appears as a local address in the query message. We can't simply copy
-    // this address to a response message and use it as a source address.
-    // Instead we will need to use the address assigned to the interface
-    // on which the query has been received. In other cases, we will just
-    // use this address as a source address for the response.
-    // Do the same for DHCPv4-over-DHCPv6 exchanges.
-    if (local_addr.isV4Bcast() || query->isDhcp4o6()) {
-        SocketInfo sock_info = IfaceMgr::instance().getSocket(*query);
-        local_addr = sock_info.addr_;
+        response->setLocalAddr(IOAddress::IPV4_ZERO_ADDRESS());
+        response->setIface("");
+        response->resetIndex();
+
+    } else {
+
+        IOAddress local_addr = query->getLocalAddr();
+
+        // In many cases the query is sent to a broadcast address. This address
+        // appears as a local address in the query message. We can't simply copy
+        // this address to a response message and use it as a source address.
+        // Instead we will need to use the address assigned to the interface
+        // on which the query has been received. In other cases, we will just
+        // use this address as a source address for the response.
+        // Do the same for DHCPv4-over-DHCPv6 exchanges.
+        if (local_addr.isV4Bcast() || query->isDhcp4o6()) {
+            SocketInfo sock_info = IfaceMgr::instance().getSocket(*query);
+            local_addr = sock_info.addr_;
+        }
+
+        // We assume that there is an appropriate socket bound to this address
+        // and that the address is correct. This is safe assumption because
+        // the local address of the query is set when the query is received.
+        // The query sent to an incorrect address wouldn't have been received.
+        // However, if socket is closed for this address between the reception
+        // of the query and sending a response, the IfaceMgr should detect it
+        // and return an error.
+        response->setLocalAddr(local_addr);
+        // In many cases the query is sent to a broadcast address. This address
+        // appears as a local address in the query message. Therefore we can't
+        // simply copy local address from the query and use it as a source
+        // address for the response. Instead, we have to check what address our
+        // socket is bound to and use it as a source address. This operation
+        // may throw if for some reason the socket is closed.
+        /// @todo Consider an optimization that we use local address from
+        /// the query if this address is not broadcast.
+        response->setIface(query->getIface());
+        response->setIndex(query->getIndex());
     }
-    // We assume that there is an appropriate socket bound to this address
-    // and that the address is correct. This is safe assumption because
-    // the local address of the query is set when the query is received.
-    // The query sent to an incorrect address wouldn't have been received.
-    // However, if socket is closed for this address between the reception
-    // of the query and sending a response, the IfaceMgr should detect it
-    // and return an error.
-    response->setLocalAddr(local_addr);
-    // In many cases the query is sent to a broadcast address. This address
-    // appears as a local address in the query message. Therefore we can't
-    // simply copy local address from the query and use it as a source
-    // address for the response. Instead, we have to check what address our
-    // socket is bound to and use it as a source address. This operation
-    // may throw if for some reason the socket is closed.
-    /// @todo Consider an optimization that we use local address from
-    /// the query if this address is not broadcast.
+
     response->setLocalPort(DHCP4_SERVER_PORT);
-    response->setIface(query->getIface());
-    response->setIndex(query->getIndex());
 }
 
 void

+ 4 - 6
src/bin/dhcp4/dhcp4_srv.h

@@ -681,12 +681,10 @@ protected:
 
     /// @brief Adds server identifier option to the server's response.
     ///
-    /// This method adds a server identifier to the DHCPv4 message. It expects
-    /// that the local (source) address is set for this message. If address is
-    /// not set, it will throw an exception. This method also expects that the
-    /// server identifier option is not present in the specified message.
-    /// Otherwise, it will throw an exception on attempt to add a duplicate
-    /// server identifier option.
+    /// This method adds a server identifier to the DHCPv4 message. This is set
+    /// to the local address on which the client's query has been received with
+    /// the exception of broadcast traffic and DHCPv4o6 query for which a socket
+    /// on the particular interface is found and its address is used as server id.
     ///
     /// @note This method doesn't throw exceptions by itself but the underlying
     /// classes being used my throw. The reason for this method to not sanity

+ 2 - 0
src/bin/dhcp4/parser_context.cc

@@ -144,6 +144,8 @@ Parser4Context::contextName()
         return ("interfaces-config");
     case DHCP_SOCKET_TYPE:
         return ("dhcp-socket-type");
+    case OUTBOUND_INTERFACE:
+        return ("outbound-interface");
     case LEASE_DATABASE:
         return ("lease-database");
     case HOSTS_DATABASE:

+ 3 - 0
src/bin/dhcp4/parser_context.h

@@ -214,6 +214,9 @@ public:
         /// Used while parsing Dhcp4/interfaces/dhcp-socket-type structures.
         DHCP_SOCKET_TYPE,
 
+        /// Used while parsing Dhcp4/interfaces/outbound-interface structures.
+        OUTBOUND_INTERFACE,
+
         /// Used while parsing Dhcp4/lease-database structures.
         LEASE_DATABASE,
 

+ 83 - 1
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc

@@ -204,6 +204,88 @@ TEST_F(Dhcpv4SrvTest, adjustIfaceDataRelay) {
     EXPECT_EQ("192.0.1.50", resp->getRemoteAddr().toText());
 }
 
+// This test verifies that it is possible to configure the server to use
+// routing information to determine the right outbound interface to sent
+// responses to a relayed client.
+TEST_F(Dhcpv4SrvTest, adjustIfaceDataUseRouting) {
+    IfaceMgrTestConfig test_config(true);
+    IfaceMgr::instance().openSockets4();
+
+    // Create configuration for interfaces. It includes the outbound-interface
+    // setting which indicates that the responses aren't neccessarily sent
+    // over the same interface via which a request has been received, but routing
+    // information is used to determine this interface.
+    CfgMgr::instance().clear();
+    CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+    cfg_iface->useSocketType(AF_INET, CfgIface::SOCKET_UDP);
+    cfg_iface->use(AF_INET, "eth0");
+    cfg_iface->use(AF_INET, "eth1");
+    cfg_iface->setOutboundIface(CfgIface::USE_ROUTING);
+    CfgMgr::instance().commit();;
+
+    // Create the instance of the incoming packet.
+    boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+    // Set the giaddr to non-zero address and hops to non-zero value
+    // as if it was relayed.
+    req->setGiaddr(IOAddress("192.0.1.1"));
+    req->setHops(2);
+    // Set ciaddr to zero. This simulates the client which applies
+    // for the new lease.
+    req->setCiaddr(IOAddress("0.0.0.0"));
+    // Clear broadcast flag.
+    req->setFlags(0x0000);
+
+    // Set local address, port and interface.
+    req->setLocalAddr(IOAddress("192.0.2.5"));
+    req->setLocalPort(1001);
+    req->setIface("eth1");
+    req->setIndex(1);
+
+    // Create the exchange using the req.
+    Dhcpv4Exchange ex = createExchange(req);
+
+    Pkt4Ptr resp = ex.getResponse();
+    resp->setYiaddr(IOAddress("192.0.1.100"));
+    // Clear the remote address.
+    resp->setRemoteAddr(IOAddress("0.0.0.0"));
+    // Set hops value for the response.
+    resp->setHops(req->getHops());
+
+    // This function never throws.
+    ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(ex));
+
+    // Now the destination address should be relay's address.
+    EXPECT_EQ("192.0.1.1", resp->getRemoteAddr().toText());
+    // The query has been relayed, so the response must be sent to the port 67.
+    EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+
+    // The local port is always DHCPv4 server port 67.
+    EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+
+    // No specific interface is selected as outbound interface and no specific
+    // local address is provided. The IfaceMgr will figure out which interface to use.
+    EXPECT_TRUE(resp->getLocalAddr().isV4Zero());
+    EXPECT_TRUE(resp->getIface().empty());
+    EXPECT_FALSE(resp->indexSet());
+
+    // Another test verifies that setting outbound interface to same as inbound will
+    // cause the server to set interface and local address as expected.
+
+    cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+    cfg_iface->useSocketType(AF_INET, CfgIface::SOCKET_UDP);
+    cfg_iface->use(AF_INET, "eth0");
+    cfg_iface->use(AF_INET, "eth1");
+    cfg_iface->setOutboundIface(CfgIface::SAME_AS_INBOUND);
+    CfgMgr::instance().commit();
+
+    ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(ex));
+
+    EXPECT_EQ("192.0.2.5", resp->getLocalAddr().toText());
+    EXPECT_EQ("eth1", resp->getIface());
+    EXPECT_EQ(1, resp->getIndex());
+}
+
 // This test verifies that the destination address of the response message
 // is set to ciaddr when giaddr is set to zero and the ciaddr is set to
 // non-zero address in the received message. This is the case when the
@@ -487,7 +569,7 @@ TEST_F(Dhcpv4SrvTest, appendServerID) {
 
     // Set a local address. It is required by the function under test
     // to create the Server Identifier option.
-    response->setLocalAddr(IOAddress("192.0.3.1"));
+    query->setLocalAddr(IOAddress("192.0.3.1"));
 
     // Append the Server Identifier.
     ASSERT_NO_THROW(NakedDhcpv4Srv::appendServerID(ex));

+ 19 - 0
src/bin/dhcp4/tests/parser_unittest.cc

@@ -261,6 +261,25 @@ TEST(ParserTest, file) {
     }
 }
 
+// Basic test that checks if it's possible to specify outbound-interface.
+TEST(ParserTest, outboundIface) {
+    std::string fname = string(CFG_EXAMPLES) + "/" + "advanced.json";
+    Parser4Context ctx;
+    ConstElementPtr test_json = ctx.parseFile(fname, Parser4Context::PARSER_DHCP4);
+
+    ConstElementPtr tmp;
+    tmp = test_json->get("Dhcp4");
+    ASSERT_TRUE(tmp);
+
+    tmp = tmp->get("interfaces-config");
+    ASSERT_TRUE(tmp);
+
+    tmp = tmp->get("outbound-interface");
+    ASSERT_TRUE(tmp);
+    EXPECT_EQ(Element::string, tmp->getType());
+    EXPECT_EQ("use-routing", tmp->stringValue());
+}
+
 /// @brief Tests error conditions in Dhcp4Parser
 ///
 /// @param txt text to be parsed

+ 13 - 1
src/lib/dhcp/pkt.h

@@ -468,6 +468,11 @@ public:
         ifindex_ = ifindex;
     };
 
+    /// @brief Resets interface index to negative value.
+    void resetIndex() {
+        ifindex_ = -1;
+    }
+
     /// @brief Returns interface index.
     ///
     /// @return interface index
@@ -475,6 +480,13 @@ public:
         return (ifindex_);
     };
 
+    /// @brief Checks if interface index has been set.
+    ///
+    /// @return true if interface index set, false otherwise.
+    bool indexSet() const {
+        return (ifindex_ >= 0);
+    }
+
     /// @brief Returns interface name.
     ///
     /// Returns interface name over which packet was received or is
@@ -697,7 +709,7 @@ protected:
     /// Each network interface has assigned an unique ifindex.
     /// It is a functional equivalent of a name, but sometimes more useful, e.g.
     /// when using odd systems that allow spaces in interface names.
-    int ifindex_;
+    int64_t ifindex_;
 
     /// @brief Local IP (v4 or v6) address.
     ///

+ 16 - 3
src/lib/dhcp/pkt_filter_inet.cc

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -255,8 +255,21 @@ PktFilterInet::send(const Iface&, uint16_t sockfd,
     cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
     struct in_pktinfo* pktinfo =(struct in_pktinfo *)CMSG_DATA(cmsg);
     memset(pktinfo, 0, sizeof(struct in_pktinfo));
-    pktinfo->ipi_ifindex = pkt->getIndex();
-    pktinfo->ipi_spec_dst.s_addr = htonl(pkt->getLocalAddr().toUint32()); // set the source IP address
+
+    // In some cases the index of the outbound interface is not set. This
+    // is a matter of configuration. When the server is configured to
+    // determine the outbound interface based on routing information,
+    // the index is left unset (negative).
+    if (pkt->indexSet()) {
+        pktinfo->ipi_ifindex = pkt->getIndex();
+    }
+
+    // When the DHCP server is using routing to determine the outbound
+    // interface, the local address is also left unset.
+    if (!pkt->getLocalAddr().isV4Zero()) {
+        pktinfo->ipi_spec_dst.s_addr = htonl(pkt->getLocalAddr().toUint32());
+    }
+
     m.msg_controllen = CMSG_SPACE(sizeof(struct in_pktinfo));
 #endif
 

+ 7 - 0
src/lib/dhcp/pkt_filter_inet.h

@@ -67,6 +67,13 @@ public:
 
     /// @brief Send packet over specified socket.
     ///
+    /// This function will use local address specified in the @c pkt as a source
+    /// address for the packet and the interface index to select the index
+    /// through which the packet will be sent. However, if these values
+    /// are not specified for the packet (zero IP address and negative
+    /// interface index), this function will rely on the routing information
+    /// to determine the right outbound interface and source address.
+    ///
     /// @param iface interface to be used to send packet
     /// @param sockfd socket descriptor
     /// @param pkt packet to be sent

+ 43 - 1
src/lib/dhcpsrv/cfg_iface.cc

@@ -22,7 +22,8 @@ namespace dhcp {
 const char* CfgIface::ALL_IFACES_KEYWORD = "*";
 
 CfgIface::CfgIface()
-    : wildcard_used_(false), socket_type_(SOCKET_RAW), re_detect_(false) {
+    : wildcard_used_(false), socket_type_(SOCKET_RAW), re_detect_(false),
+      outbound_iface_(SAME_AS_INBOUND) {
 }
 
 void
@@ -226,6 +227,43 @@ CfgIface::textToSocketType(const std::string& socket_type_name) const {
     }
 }
 
+CfgIface::OutboundIface
+CfgIface::getOutboundIface() const {
+    return (outbound_iface_);
+}
+
+std::string
+CfgIface::outboundTypeToText() const {
+    switch (outbound_iface_) {
+    case SAME_AS_INBOUND:
+        return ("same-as-inbound");
+    case USE_ROUTING:
+        return ("use-routing");
+    default:
+        isc_throw(Unexpected, "unsupported outbound-type " << socket_type_);
+    }
+
+}
+
+CfgIface::OutboundIface
+CfgIface::textToOutboundIface(const std::string& txt) {
+    if (txt == "same-as-inbound") {
+        return (SAME_AS_INBOUND);
+
+    } else if (txt == "use-routing") {
+        return (USE_ROUTING);
+
+    } else {
+        isc_throw(BadValue, "unsupported outbound interface type '"
+                  << txt << "'");
+    }
+}
+
+void
+CfgIface::setOutboundIface(const OutboundIface& outbound_iface) {
+    outbound_iface_ = outbound_iface;
+}
+
 void
 CfgIface::use(const uint16_t family, const std::string& iface_name) {
     // The interface name specified may have two formats:
@@ -425,6 +463,10 @@ CfgIface::toElement() const {
         result->set("dhcp-socket-type", Element::create(std::string("udp")));
     }
 
+    if (outbound_iface_ != SAME_AS_INBOUND) {
+        result->set("outbound-interface", Element::create(outboundTypeToText()));
+    }
+
     // Set re-detect
     result->set("re-detect", Element::create(re_detect_));
 

+ 38 - 0
src/lib/dhcpsrv/cfg_iface.h

@@ -137,6 +137,15 @@ public:
         SOCKET_UDP
     };
 
+    /// @brief Indicates how outbound interface is selected for relayed traffic.
+    enum OutboundIface {
+        /// Server sends responses over the same interface on which queries are
+        /// received.
+        SAME_AS_INBOUND,
+        /// Server uses routing to determine the right interface to send response.
+        USE_ROUTING
+    };
+
     /// @brief Keyword used to enable all interfaces.
     ///
     /// This keyword can be used instead of the interface name to specify
@@ -224,9 +233,35 @@ public:
     void useSocketType(const uint16_t family,
                        const std::string& socket_type_name);
 
+    /// @brief Returns DHCP socket type used by the server.
+    SocketType getSocketType() const {
+        return (socket_type_);
+    }
+
     /// @brief Returns the socket type in the textual format.
     std::string socketTypeToText() const;
 
+    /// @brief Sets outbound interface selection mode.
+    ///
+    /// @param outbound_iface New outbound interface selection mode setting.
+    void setOutboundIface(const OutboundIface& outbound_iface);
+
+    /// @brief Returns outbound interface selection mode.
+    ///
+    /// @return Outbound interface selection mode.
+    OutboundIface getOutboundIface() const;
+
+    /// @brief Returns outbound interface selection mode as string.
+    ///
+    /// @return text representation of the outbound interface selection mode.
+    std::string outboundTypeToText() const;
+
+    /// @brief Converts text to outbound interface selection mode.
+    ///
+    /// @param txt either 'same-as-inbound' or 'use-routing'
+    /// @return Outbound interface selection mode.
+    static OutboundIface textToOutboundIface(const std::string& txt);
+
     /// @brief Converts the socket type in the textual format to the type
     /// represented by the @c SocketType.
     ///
@@ -340,6 +375,9 @@ private:
 
     /// @brief A boolean value which reflects current re-detect setting
     bool re_detect_;
+
+    /// @brief Indicates how outbound interface is selected for relayed traffic.
+    OutboundIface outbound_iface_;
 };
 
 /// @brief A pointer to the @c CfgIface .

+ 12 - 0
src/lib/dhcpsrv/parsers/ifaces_config_parser.cc

@@ -76,6 +76,18 @@ IfacesConfigParser::parse(const CfgIfacePtr& cfg,
                 }
             }
 
+            if (element.first == "outbound-interface") {
+                if (protocol_ == AF_INET) {
+                    CfgIface::OutboundIface type =
+                        CfgIface::textToOutboundIface(element.second->stringValue());
+                    cfg->setOutboundIface(type);
+                    continue;
+                } else {
+                    isc_throw(DhcpConfigError,
+                              "outbound-interface is not supported in DHCPv6");
+                }
+            }
+
             // This should never happen as the input produced by the parser
             // see (src/bin/dhcpX/dhcpX_parser.yy) should not produce any
             // other parameter, so this case is only to catch bugs in

+ 67 - 0
src/lib/dhcpsrv/tests/ifaces_config_parser_unittest.cc

@@ -98,6 +98,33 @@ TEST_F(IfacesConfigParserTest, interfaces) {
     EXPECT_TRUE(test_config.socketOpen("eth1", AF_INET));
 }
 
+
+// This test checks that the parsed structure can be converted back to Element
+// tree.
+TEST_F(IfacesConfigParserTest, toElement) {
+    // Creates fake interfaces with fake addresses.
+    IfaceMgrTestConfig test_config(true);
+
+    // Configuration with one interface.
+    std::string config =
+        "{ \"interfaces\": [ \"eth0\" ], "
+        "  \"dhcp-socket-type\": \"udp\","
+        "  \"outbound-interface\": \"use-routing\", "
+        "  \"re-detect\": false }";
+
+    ElementPtr config_element = Element::fromJSON(config);
+
+    // Parse the configuration.
+    IfacesConfigParser parser(AF_INET);
+    CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+    ASSERT_TRUE(cfg_iface);
+    ASSERT_NO_THROW(parser.parse(cfg_iface, config_element));
+
+    // Check it can be unparsed.
+    runToElementTest<CfgIface>(config, *cfg_iface);
+}
+
+
 // This test verifies that it is possible to select the raw socket
 // use in the configuration for interfaces.
 TEST_F(IfacesConfigParserTest, socketTypeRaw) {
@@ -177,4 +204,44 @@ TEST_F(IfacesConfigParserTest, socketTypeInvalid) {
     ASSERT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
 }
 
+// Tests that outbound-interface is parsed properly.
+TEST_F(IfacesConfigParserTest, outboundInterface) {
+    // For DHCPv4 we accept 'use-routing' or 'same-as-inbound'.
+    IfacesConfigParser parser4(AF_INET);
+
+    // For DHCPv6 we don't accept this at all.
+    IfacesConfigParser parser6(AF_INET6);
+
+    CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
+
+    // The default should be to use the same as client's query packet.
+    EXPECT_EQ(CfgIface::SAME_AS_INBOUND, cfg_iface->getOutboundIface());
+
+    // Value 1: use-routing
+    std::string config = "{ \"interfaces\": [ ],"
+        "\"outbound-interface\": \"use-routing\","
+        " \"re-detect\": false }";
+    ElementPtr config_element = Element::fromJSON(config);
+    ASSERT_NO_THROW(parser4.parse(cfg_iface, config_element));
+    EXPECT_EQ(CfgIface::USE_ROUTING, cfg_iface->getOutboundIface());
+    EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+
+    // Value 2: same-as-inbound
+    config = "{ \"interfaces\": [ ],"
+        "\"outbound-interface\": \"same-as-inbound\","
+        " \"re-detect\": false }";
+    config_element = Element::fromJSON(config);
+    ASSERT_NO_THROW(parser4.parse(cfg_iface, config_element));
+    EXPECT_EQ(CfgIface::SAME_AS_INBOUND, cfg_iface->getOutboundIface());
+    EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+
+    // Other values are not supported.
+    config = "{ \"interfaces\": [ ],"
+        "\"outbound-interface\": \"default\","
+        " \"re-detect\": false }";
+    config_element = Element::fromJSON(config);
+    EXPECT_THROW(parser4.parse(cfg_iface, config_element), DhcpConfigError);
+    EXPECT_THROW(parser6.parse(cfg_iface, config_element), DhcpConfigError);
+}
+
 } // end of anonymous namespace