Commit ea193899 authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[master] Merge branch 'trac3195' (DHCPv6 unicast sockets)

Conflicts:
	ChangeLog
parents 3d320e51 72e601f2
695. [func] tomek
b10-dhcp6 is now able to listen on global IPv6 unicast addresses.
(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
694. [bug] tomek
b10-dhcp6 now handles exceptions better when processing initial
configuration. In particular, errors with socket binding do not
prevent b10-dhcp6 from establishing configuration session anymore.
(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
693. [bug] tomek
b10-dhcp6 now handles IPv6 interface enabling correctly.
(Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
692. [bug] marcin 692. [bug] marcin
b10-dhcp4: Fix a bug whereby the Parameter Request List was not parsed b10-dhcp4: Fix a bug whereby the Parameter Request List was not parsed
by the server and requested DHCPv4 options were not returned to the by the server and requested DHCPv4 options were not returned to the
......
...@@ -4669,6 +4669,43 @@ Dhcp6/subnet6/ list ...@@ -4669,6 +4669,43 @@ Dhcp6/subnet6/ list
</para> </para>
</section> </section>
<section id="dhcp6-unicast">
<title>Unicast traffic support</title>
<para>
When DHCPv6 server starts up, by default it listens to the DHCP traffic
sent to multicast address ff02::1:2 on each interface that it is
configured to listen on (see <xref linkend="dhcp6-interface-selection"/>).
In some cases it is useful to configure a server to handle incoming
traffic sent to the global unicast addresses as well. The most common
reason for that is to have relays send their traffic to the server
directly. To configure server to listen on specific unicast address, a
notation to specify interfaces has been extended. Interface name can be
optionally followed by a slash, followed by global unicast address that
server should listen on. That will be done in addition to normal
link-local binding + listening on ff02::1:2 address. The sample commands
listed below show how to listen on 2001:db8::1 (a global address)
configured on the eth1 interface.
</para>
<para>
<screen>
&gt; <userinput>config set Dhcp6/interfaces[0] eth1/2001:db8::1</userinput>
&gt; <userinput>config commit</userinput></screen>
When configuration gets committed, the server will start to listen on
eth1 on link-local address, mutlicast group (ff02::1:2) and 2001:db8::1.
</para>
<para>
It is possible to mix interface names, wildcards and interface name/addresses
on the Dhcp6/interface list. It is not possible to specify more than one
unicast address on a given interface.
</para>
<para>
Care should be taken to specify proper unicast addresses. The server will
attempt to bind to those addresses specified, without any additional checks.
That approach is selected on purpose, so in the software can be used to
communicate over uncommon addresses if the administrator desires so.
</para>
</section>
<section> <section>
<title>Subnet and Address Pool</title> <title>Subnet and Address Pool</title>
<para> <para>
......
...@@ -216,7 +216,7 @@ void ControlledDhcpv6Srv::establishSession() { ...@@ -216,7 +216,7 @@ void ControlledDhcpv6Srv::establishSession() {
// reopen sockets according to new configuration. // reopen sockets according to new configuration.
openActiveSockets(getPort()); openActiveSockets(getPort());
} catch (const DhcpConfigError& ex) { } catch (const std::exception& ex) {
LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what()); LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
} }
......
...@@ -450,6 +450,9 @@ This debug message indicates that a shutdown of the IPv6 server has ...@@ -450,6 +450,9 @@ This debug message indicates that a shutdown of the IPv6 server has
been requested via a call to the 'shutdown' method of the core Dhcpv6Srv been requested via a call to the 'shutdown' method of the core Dhcpv6Srv
object. object.
% DHCP6_SOCKET_UNICAST server is about to open socket on address %1 on interface %2
This is a debug message that inform that a unicast socket will be opened.
% DHCP6_SRV_CONSTRUCT_ERROR error creating Dhcpv6Srv object, reason: %1 % DHCP6_SRV_CONSTRUCT_ERROR error creating Dhcpv6Srv object, reason: %1
This error message indicates that during startup, the construction of a This error message indicates that during startup, the construction of a
core component within the IPv6 DHCP server (the Dhcpv6 server object) core component within the IPv6 DHCP server (the Dhcpv6 server object)
......
...@@ -2229,7 +2229,7 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) { ...@@ -2229,7 +2229,7 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) {
<< " trying to reopen sockets after reconfiguration"); << " trying to reopen sockets after reconfiguration");
} }
if (CfgMgr::instance().isActiveIface(iface->getName())) { if (CfgMgr::instance().isActiveIface(iface->getName())) {
iface_ptr->inactive4_ = false; iface_ptr->inactive6_ = false;
LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE) LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
.arg(iface->getFullName()); .arg(iface->getFullName());
...@@ -2242,6 +2242,15 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) { ...@@ -2242,6 +2242,15 @@ Dhcpv6Srv::openActiveSockets(const uint16_t port) {
iface_ptr->inactive6_ = true; iface_ptr->inactive6_ = true;
} }
iface_ptr->clearUnicasts();
const IOAddress* unicast = CfgMgr::instance().getUnicast(iface->getName());
if (unicast) {
LOG_INFO(dhcp6_logger, DHCP6_SOCKET_UNICAST).arg(unicast->toText())
.arg(iface->getName());
iface_ptr->addUnicast(*unicast);
}
} }
// Let's reopen active sockets. openSockets6 will check internally whether // Let's reopen active sockets. openSockets6 will check internally whether
// sockets are marked active or inactive. // sockets are marked active or inactive.
......
...@@ -177,6 +177,17 @@ IfaceMgr::IfaceMgr() ...@@ -177,6 +177,17 @@ IfaceMgr::IfaceMgr()
} }
} }
void Iface::addUnicast(const isc::asiolink::IOAddress& addr) {
for (Iface::AddressCollection::const_iterator i = unicasts_.begin();
i != unicasts_.end(); ++i) {
if (*i == addr) {
isc_throw(BadValue, "Address " << addr.toText()
<< " already defined on the " << name_ << " interface.");
}
}
unicasts_.push_back(addr);
}
void IfaceMgr::closeSockets() { void IfaceMgr::closeSockets() {
for (IfaceCollection::iterator iface = ifaces_.begin(); for (IfaceCollection::iterator iface = ifaces_.begin();
iface != ifaces_.end(); ++iface) { iface != ifaces_.end(); ++iface) {
...@@ -343,8 +354,10 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) { ...@@ -343,8 +354,10 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
} }
if (sock < 0) { if (sock < 0) {
const char* errstr = strerror(errno);
isc_throw(SocketConfigError, "failed to open IPv4 socket" isc_throw(SocketConfigError, "failed to open IPv4 socket"
<< " supporting broadcast traffic"); << " supporting broadcast traffic, reason:"
<< errstr);
} }
count++; count++;
...@@ -368,6 +381,23 @@ bool IfaceMgr::openSockets6(const uint16_t port) { ...@@ -368,6 +381,23 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
continue; continue;
} }
// Open unicast sockets if there are any unicast addresses defined
Iface::AddressCollection unicasts = iface->getUnicasts();
for (Iface::AddressCollection::iterator addr = unicasts.begin();
addr != unicasts.end(); ++addr) {
sock = openSocket(iface->getName(), *addr, port);
if (sock < 0) {
const char* errstr = strerror(errno);
isc_throw(SocketConfigError, "failed to open unicast socket on "
<< addr->toText() << " on interface " << iface->getName()
<< ", reason: " << errstr);
}
count++;
}
Iface::AddressCollection addrs = iface->getAddresses(); Iface::AddressCollection addrs = iface->getAddresses();
for (Iface::AddressCollection::iterator addr = addrs.begin(); for (Iface::AddressCollection::iterator addr = addrs.begin();
addr != addrs.end(); addr != addrs.end();
...@@ -389,7 +419,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) { ...@@ -389,7 +419,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
sock = openSocket(iface->getName(), *addr, port); sock = openSocket(iface->getName(), *addr, port);
if (sock < 0) { if (sock < 0) {
isc_throw(SocketConfigError, "failed to open unicast socket"); const char* errstr = strerror(errno);
isc_throw(SocketConfigError, "failed to open link-local socket on "
<< addr->toText() << " on interface "
<< iface->getName() << ", reason: " << errstr);
} }
// Binding socket to unicast address and then joining multicast group // Binding socket to unicast address and then joining multicast group
...@@ -414,8 +447,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) { ...@@ -414,8 +447,10 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS), IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS),
port); port);
if (sock2 < 0) { if (sock2 < 0) {
const char* errstr = strerror(errno);
isc_throw(SocketConfigError, "Failed to open multicast socket on " isc_throw(SocketConfigError, "Failed to open multicast socket on "
<< " interface " << iface->getFullName()); << " interface " << iface->getFullName() << ", reason:"
<< errstr);
iface->delSocket(sock); // delete previously opened socket iface->delSocket(sock); // delete previously opened socket
} }
#endif #endif
...@@ -608,7 +643,9 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) { ...@@ -608,7 +643,9 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) {
// interface. // interface.
sock.open(asio::ip::udp::v4(), err_code); sock.open(asio::ip::udp::v4(), err_code);
if (err_code) { if (err_code) {
isc_throw(Unexpected, "failed to open UDPv4 socket"); const char* errstr = strerror(errno);
isc_throw(Unexpected, "failed to open UDPv4 socket, reason:"
<< errstr);
} }
sock.set_option(asio::socket_base::broadcast(true), err_code); sock.set_option(asio::socket_base::broadcast(true), err_code);
if (err_code) { if (err_code) {
...@@ -1137,16 +1174,50 @@ uint16_t IfaceMgr::getSocket(const isc::dhcp::Pkt6& pkt) { ...@@ -1137,16 +1174,50 @@ uint16_t IfaceMgr::getSocket(const isc::dhcp::Pkt6& pkt) {
<< pkt.getIface()); << pkt.getIface());
} }
const Iface::SocketCollection& socket_collection = iface->getSockets(); const Iface::SocketCollection& socket_collection = iface->getSockets();
Iface::SocketCollection::const_iterator candidate = socket_collection.end();
Iface::SocketCollection::const_iterator s; Iface::SocketCollection::const_iterator s;
for (s = socket_collection.begin(); s != socket_collection.end(); ++s) { for (s = socket_collection.begin(); s != socket_collection.end(); ++s) {
if ((s->family_ == AF_INET6) &&
(!s->addr_.getAddress().to_v6().is_multicast())) { // We should not merge those conditions for debugging reasons.
// V4 sockets are useless for sending v6 packets.
if (s->family_ != AF_INET6) {
continue;
}
// Sockets bound to multicast address are useless for sending anything.
if (s->addr_.getAddress().to_v6().is_multicast()) {
continue;
}
if (s->addr_ == pkt.getLocalAddr()) {
// This socket is bound to the source address. This is perfect
// match, no need to look any further.
return (s->sockfd_); return (s->sockfd_);
} }
/// @todo: Add more checks here later. If remote address is
/// not link-local, we can't use link local bound socket // If we don't have any other candidate, this one will do
/// to send data. if (candidate == socket_collection.end()) {
candidate = s;
} else {
// If we want to send something to link-local and the socket is
// bound to link-local or we want to send to global and the socket
// is bound to global, then use it as candidate
if ( (pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
s->addr_.getAddress().to_v6().is_link_local()) ||
(!pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
!s->addr_.getAddress().to_v6().is_link_local()) ) {
candidate = s;
}
}
}
if (candidate != socket_collection.end()) {
return (candidate->sockfd_);
} }
isc_throw(Unexpected, "Interface " << iface->getFullName() isc_throw(Unexpected, "Interface " << iface->getFullName()
...@@ -1175,5 +1246,6 @@ uint16_t IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) { ...@@ -1175,5 +1246,6 @@ uint16_t IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) {
<< " does not have any suitable IPv4 sockets open."); << " does not have any suitable IPv4 sockets open.");
} }
} // end of namespace isc::dhcp } // end of namespace isc::dhcp
} // end of namespace isc } // end of namespace isc
...@@ -264,6 +264,27 @@ public: ...@@ -264,6 +264,27 @@ public:
/// @return collection of sockets added to interface /// @return collection of sockets added to interface
const SocketCollection& getSockets() const { return sockets_; } const SocketCollection& getSockets() const { return sockets_; }
/// @brief Removes any unicast addresses
///
/// Removes any unicast addresses that the server was configured to
/// listen on
void clearUnicasts() {
unicasts_.clear();
}
/// @brief Adds unicast the server should listen on
///
/// @throw BadValue if specified address is already defined on interface
/// @param addr unicast address to listen on
void addUnicast(const isc::asiolink::IOAddress& addr);
/// @brief Returns a container of addresses the server should listen on
///
/// @return address collection (may be empty)
const AddressCollection& getUnicasts() const {
return unicasts_;
}
protected: protected:
/// Socket used to send data. /// Socket used to send data.
SocketCollection sockets_; SocketCollection sockets_;
...@@ -277,6 +298,9 @@ protected: ...@@ -277,6 +298,9 @@ protected:
/// List of assigned addresses. /// List of assigned addresses.
AddressCollection addrs_; AddressCollection addrs_;
/// List of unicast addresses the server should listen on
AddressCollection unicasts_;
/// Link-layer address. /// Link-layer address.
uint8_t mac_[MAX_MAC_LEN]; uint8_t mac_[MAX_MAC_LEN];
......
...@@ -145,6 +145,27 @@ public: ...@@ -145,6 +145,27 @@ public:
return (sockets_count); return (sockets_count);
} }
/// @brief returns socket bound to a specific address (or NULL)
///
/// A helper function, used to pick a socketinfo that is bound to a given
/// address.
///
/// @param sockets sockets collection
/// @param addr address the socket is bound to
///
/// @return socket info structure (or NULL)
const isc::dhcp::SocketInfo*
getSocketByAddr(const isc::dhcp::Iface::SocketCollection& sockets,
const IOAddress& addr) {
for (isc::dhcp::Iface::SocketCollection::const_iterator s =
sockets.begin(); s != sockets.end(); ++s) {
if (s->addr_ == addr) {
return (&(*s));
}
}
return (NULL);
}
}; };
// We need some known interface to work reliably. Loopback interface is named // We need some known interface to work reliably. Loopback interface is named
...@@ -781,6 +802,7 @@ TEST_F(IfaceMgrTest, sendReceive6) { ...@@ -781,6 +802,7 @@ TEST_F(IfaceMgrTest, sendReceive6) {
// try to send/receive data over the closed socket. Closed socket's descriptor is // try to send/receive data over the closed socket. Closed socket's descriptor is
// still being hold by IfaceMgr which will try to use it to receive data. // still being hold by IfaceMgr which will try to use it to receive data.
close(socket1); close(socket1);
close(socket2);
EXPECT_THROW(ifacemgr->receive6(10), SocketReadError); EXPECT_THROW(ifacemgr->receive6(10), SocketReadError);
EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError); EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
} }
...@@ -1520,4 +1542,116 @@ TEST_F(IfaceMgrTest, controlSession) { ...@@ -1520,4 +1542,116 @@ TEST_F(IfaceMgrTest, controlSession) {
close(pipefd[0]); close(pipefd[0]);
} }
// Test checks if the unicast sockets can be opened.
// This test is now disabled, because there is no reliable way to test it. We
// can't even use loopback, beacuse openSockets() skips loopback interface
// (as it should be, because DHCP server is not supposed to listen on loopback).
TEST_F(IfaceMgrTest, DISABLED_openUnicastSockets) {
/// @todo Need to implement a test that is able to check whether we can open
/// unicast sockets. There are 2 problems with it:
/// 1. We need to have a non-link-local address on an interface that is
/// up, running, IPv6 and multicast capable
/// 2. We need that information on every OS that we run tests on. So far
/// we are only supporting interface detection in Linux.
///
/// To achieve this, we will probably need a pre-test setup, similar to what
/// BIND9 is doing (i.e. configuring well known addresses on loopback).
scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
// Get the interface (todo: which interface)
Iface* iface = ifacemgr->getIface("eth0");
ASSERT_TRUE(iface);
iface->inactive6_ = false;
// Tell the interface that it should bind to this global interface
EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
// Tell IfaceMgr to open sockets. This should trigger at least 2 sockets
// to open on eth0: link-local and global. On some systems (Linux), an
// additional socket for multicast may be opened.
EXPECT_TRUE(ifacemgr->openSockets6(PORT1));
const Iface::SocketCollection& sockets = iface->getSockets();
ASSERT_GE(2, sockets.size());
// Global unicast should be first
EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("2001:db8::1")));
EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("figure-out-link-local-addr")));
}
// Checks if there is a protection against unicast duplicates.
TEST_F(IfaceMgrTest, unicastDuplicates) {
NakedIfaceMgr ifacemgr;
Iface* iface = ifacemgr.getIface(LOOPBACK);
if (iface == NULL) {
cout << "Local loopback interface not found. Skipping test. " << endl;
return;
}
// Tell the interface that it should bind to this global interface
EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
// Tell the interface that it should bind to this global interface
EXPECT_THROW(iface->addUnicast(IOAddress("2001:db8::1")), BadValue);
}
// This test requires addresses 2001:db8:15c::1/128 and fe80::1/64 to be
// configured on loopback interface
//
// Useful commands:
// ip a a 2001:db8:15c::1/128 dev lo
// ip a a fe80::1/64 dev lo
//
// If you do not issue those commands before running this test, it will fail.
TEST_F(IfaceMgrTest, DISABLED_getSocket) {
// Testing socket operation in a portable way is tricky
// without interface detection implemented.
scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
IOAddress lo_addr("::1");
IOAddress link_local("fe80::1");
IOAddress global("2001:db8:15c::1");
IOAddress dst_link_local("fe80::dead:beef");
IOAddress dst_global("2001:db8:15c::dead:beef");
// Bind loopback address
int socket1 = ifacemgr->openSocket(LOOPBACK, lo_addr, 10547);
EXPECT_GE(socket1, 0); // socket >= 0
// Bind link-local address
int socket2 = ifacemgr->openSocket(LOOPBACK, link_local, 10547);
EXPECT_GE(socket2, 0);
int socket3 = ifacemgr->openSocket(LOOPBACK, global, 10547);
EXPECT_GE(socket3, 0);
// Let's make sure those sockets are unique
EXPECT_NE(socket1, socket2);
EXPECT_NE(socket2, socket3);
EXPECT_NE(socket3, socket1);
// Create a packet
Pkt6 pkt6(DHCPV6_SOLICIT, 123);
pkt6.setIface(LOOPBACK);
// Check that packets sent to link-local will get socket bound to link local
pkt6.setLocalAddr(global);
pkt6.setRemoteAddr(dst_global);
EXPECT_EQ(socket3, ifacemgr->getSocket(pkt6));
// Check that packets sent to link-local will get socket bound to link local
pkt6.setLocalAddr(link_local);
pkt6.setRemoteAddr(dst_link_local);
EXPECT_EQ(socket2, ifacemgr->getSocket(pkt6));
// Close sockets here because the following tests will want to
// open sockets on the same ports.
ifacemgr->closeSockets();
}
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include <dhcp/libdhcp++.h> #include <dhcp/libdhcp++.h>
#include <dhcpsrv/cfgmgr.h> #include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/dhcpsrv_log.h> #include <dhcpsrv/dhcpsrv_log.h>
#include <string>
using namespace isc::asiolink; using namespace isc::asiolink;
using namespace isc::util; using namespace isc::util;
...@@ -269,14 +270,31 @@ std::string CfgMgr::getDataDir() { ...@@ -269,14 +270,31 @@ std::string CfgMgr::getDataDir() {
void void
CfgMgr::addActiveIface(const std::string& iface) { CfgMgr::addActiveIface(const std::string& iface) {
if (isIfaceListedActive(iface)) {
size_t pos = iface.find("/");
std::string iface_copy = iface;
if (pos != std::string::npos) {
std::string addr_string = iface.substr(pos + 1);
try {
IOAddress addr(addr_string);
iface_copy = iface.substr(0,pos);
unicast_addrs_.insert(make_pair(iface_copy, addr));
} catch (...) {
isc_throw(BadValue, "Can't convert '" << addr_string
<< "' into address in interface defition ('"
<< iface << "')");
}
}
if (isIfaceListedActive(iface_copy)) {
isc_throw(DuplicateListeningIface, isc_throw(DuplicateListeningIface,
"attempt to add duplicate interface '" << iface << "'" "attempt to add duplicate interface '" << iface_copy << "'"
" to the set of interfaces on which server listens"); " to the set of interfaces on which server listens");
} }
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE) LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE)
.arg(iface); .arg(iface_copy);
active_ifaces_.push_back(iface); active_ifaces_.push_back(iface_copy);
} }
void void
...@@ -292,6 +310,8 @@ CfgMgr::deleteActiveIfaces() { ...@@ -292,6 +310,8 @@ CfgMgr::deleteActiveIfaces() {
DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES); DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
active_ifaces_.clear(); active_ifaces_.clear();
all_ifaces_active_ = false; all_ifaces_active_ = false;
unicast_addrs_.clear();
} }
bool bool
...@@ -319,6 +339,15 @@ CfgMgr::isIfaceListedActive(const std::string& iface) const { ...@@ -319,6 +339,15 @@ CfgMgr::isIfaceListedActive(const std::string& iface) const {
return (false); return (false);
} }
const isc::asiolink::IOAddress*
CfgMgr::getUnicast(const std::string& iface) const {
UnicastIfacesCollection::const_iterator addr = unicast_addrs_.find(iface);
if (addr == unicast_addrs_.end()) {