iface_mgr_linux.cc 20.8 KB
Newer Older
1
// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
2 3 4 5 6 7 8 9 10 11 12 13 14
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
/// @file
/// Access to interface information on Linux is via netlink, a socket-based
/// method for transferring information between the kernel and user processes.
///
/// For detailed information about netlink interface, please refer to
/// http://en.wikipedia.org/wiki/Netlink and RFC3549.  Comments in the
/// detectIfaces() method (towards the end of this file) provide an overview
/// on how the netlink interface is used here.
///
/// Note that this interface is very robust and allows many operations:
/// add/get/set/delete links, addresses, routes, queuing, manipulation of
/// traffic classes, manipulation of neighbourhood tables and even the ability
/// to do something with address labels. Getting a list of interfaces with
/// addresses configured on it is just a small subset of all possible actions.

30 31 32 33
#include <config.h>

#if defined(OS_LINUX)

34
#include <asiolink/io_address.h>
35 36
#include <dhcp/iface_mgr.h>
#include <exceptions/exceptions.h>
37 38 39 40
#include <util/io/sockaddr_util.h>

#include <boost/array.hpp>
#include <boost/static_assert.hpp>
41

42
#include <stdint.h>
43 44 45 46 47 48 49
#include <net/if.h>
#include <linux/rtnetlink.h>

using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
50
using namespace isc::util::io::internal;
51

52 53
BOOST_STATIC_ASSERT(IFLA_MAX>=IFA_MAX);

Tomek Mrugalski's avatar
Tomek Mrugalski committed
54
namespace {
55 56 57

/// @brief This class offers utility methods for netlink connection.
///
58 59
/// See IfaceMgr::detectIfaces() (Linux implementation, towards the end of this
/// file) for example usage.
60 61 62
class Netlink
{
public:
63

64
/// @brief Holds pointers to netlink messages.
Tomek Mrugalski's avatar
Tomek Mrugalski committed
65 66
///
/// netlink (a Linux interface for getting information about network
67 68 69 70 71 72 73 74
/// interfaces) uses memory aliasing. Linux kernel returns a memory
/// blob that should be interpreted as series of nlmessages. There
/// are different nlmsg structures defined with varying size. They
/// have one thing common - inital fields are laid out in the same
/// way as nlmsghdr. Therefore different messages can be represented
/// as nlmsghdr with followed variable number of bytes that are
/// message-specific. The only reasonable way to represent this in
/// C++ is to use vector of pointers to nlmsghdr (the common structure).
75
    typedef vector<nlmsghdr*> NetlinkMessages;
76 77

/// @brief Holds pointers to interface or address attributes.
78
///
79
/// Note that to get address info, a shorter (IFA_MAX rather than IFLA_MAX)
80
/// table could be used, but we will use the bigger one anyway to
81 82 83 84 85 86 87 88 89
/// make the code reusable.
///
/// rtattr is a generic structure, similar to sockaddr. It is defined
/// in linux/rtnetlink.h and shown here for documentation purposes only:
///
/// struct rtattr {
///     unsigned short<>rta_len;
///     unsigned short<>rta_type;
/// };
90
    typedef boost::array<struct rtattr*, IFLA_MAX + 1> RTattribPtrs;
91

92
    Netlink() : fd_(-1), seq_(0), dump_(0) {
93 94
        memset(&local_, 0, sizeof(struct sockaddr_nl));
        memset(&peer_, 0, sizeof(struct sockaddr_nl));
95
    }
96

97 98
    ~Netlink() {
        rtnl_close_socket();
99 100
    }

101

102 103 104 105 106 107 108 109 110 111
    void rtnl_open_socket();
    void rtnl_send_request(int family, int type);
    void rtnl_store_reply(NetlinkMessages& storage, const nlmsghdr* msg);
    void parse_rtattr(RTattribPtrs& table, rtattr* rta, int len);
    void ipaddrs_get(IfaceMgr::Iface& iface, NetlinkMessages& addr_info);
    void rtnl_process_reply(NetlinkMessages& info);
    void release_list(NetlinkMessages& messages);
    void rtnl_close_socket();

private:
112 113 114 115 116
    int fd_;            // Netlink file descriptor
    sockaddr_nl local_; // Local addresses
    sockaddr_nl peer_;  // Remote address
    uint32_t seq_;      // Counter used for generating unique sequence numbers
    uint32_t dump_;     // Number of expected message response
117 118
};

119 120 121 122 123
/// @brief defines a size of a sent netlink buffer
const static size_t SNDBUF_SIZE = 32768;

/// @brief defines a size of a received netlink buffer
const static size_t RCVBUF_SIZE = 32768;
124

125 126
/// @brief Opens netlink socket and initializes handle structure.
///
127
/// @throw isc::Unexpected Thrown if socket configuration fails.
128
void Netlink::rtnl_open_socket() {
129

130 131
    fd_ = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (fd_ < 0) {
132
        isc_throw(Unexpected, "Failed to create NETLINK socket.");
133
    }
134

135
    if (setsockopt(fd_, SOL_SOCKET, SO_SNDBUF, &SNDBUF_SIZE, sizeof(SNDBUF_SIZE)) < 0) {
136
        isc_throw(Unexpected, "Failed to set send buffer in NETLINK socket.");
137
    }
138

139
    if (setsockopt(fd_, SOL_SOCKET, SO_RCVBUF, &RCVBUF_SIZE, sizeof(RCVBUF_SIZE)) < 0) {
140
        isc_throw(Unexpected, "Failed to set receive buffer in NETLINK socket.");
141
    }
142

143 144
    local_.nl_family = AF_NETLINK;
    local_.nl_groups = 0;
145

146
    if (bind(fd_, convertSockAddr(&local_), sizeof(local_)) < 0) {
147
        isc_throw(Unexpected, "Failed to bind netlink socket.");
148 149
    }

150
    socklen_t addr_len = sizeof(local_);
151
    if (getsockname(fd_, convertSockAddr(&local_), &addr_len) < 0) {
152
        isc_throw(Unexpected, "Getsockname for netlink socket failed.");
153 154 155
    }

    // just 2 sanity checks and we are done
156 157
    if ( (addr_len != sizeof(local_)) ||
         (local_.nl_family != AF_NETLINK) ) {
158
        isc_throw(Unexpected, "getsockname() returned unexpected data for netlink socket.");
159 160 161
    }
}

162
/// @brief Closes netlink communication socket
163 164 165 166 167 168 169
void Netlink::rtnl_close_socket() {
    if (fd_ != -1) {
        close(fd_);
    }
    fd_ = -1;
}

170 171
/// @brief Sends request over NETLINK socket.
///
172 173
/// @param family requested information family.
/// @param type request type (RTM_GETLINK or RTM_GETADDR).
174
void Netlink::rtnl_send_request(int family, int type) {
175
    struct Req {
176 177
        nlmsghdr netlink_header;
        rtgenmsg generic;
178 179
    };
    Req req; // we need this type named for offsetof() used in assert
180
    struct sockaddr_nl nladdr;
181

182 183 184
    // do a sanity check. Verify that Req structure is aligned properly
    BOOST_STATIC_ASSERT(sizeof(nlmsghdr) == offsetof(Req, generic));

185 186
    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
187

188 189 190 191 192 193 194
    // According to netlink(7) manpage, mlmsg_seq must be set to a sequence
    // number and is used to track messages. That is just a value that is
    // opaque to kernel, and user-space code is supposed to use it to match
    // incoming responses to sent requests. That is not really useful as we
    // send a single request and get a single response at a time. However, we
    // obey the man page suggestion and just set this to monotonically
    // increasing numbers.
195
    seq_++;
196

197
    // This will be used to finding correct response (responses
198
    // sent by kernel are supposed to have the same sequence number
199
    // as the request we sent).
200
    dump_ = seq_;
201

202
    memset(&req, 0, sizeof(req));
203 204
    req.netlink_header.nlmsg_len = sizeof(req);
    req.netlink_header.nlmsg_type = type;
205
    req.netlink_header.nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;
206
    req.netlink_header.nlmsg_pid = 0;
207
    req.netlink_header.nlmsg_seq = seq_;
208
    req.generic.rtgen_family = family;
209

210
    int status =  sendto(fd_, static_cast<void*>(&req), sizeof(req), 0,
211 212
                         static_cast<struct sockaddr*>(static_cast<void*>(&nladdr)),
                         sizeof(nladdr));
213 214

    if (status<0) {
215 216
        isc_throw(Unexpected, "Failed to send " << sizeof(nladdr)
                  << " bytes over netlink socket.");
217 218 219
    }
}

220
/// @brief Appends nlmsg to a storage.
221
///
222 223 224
/// This method copies pointed nlmsg to a newly allocated memory
/// and adds it to storage.
///
225 226 227
/// @param storage A vector that holds pointers to netlink messages. The caller
///        is responsible for freeing the pointed-to messages.
/// @param msg A netlink message to be added.
228
void Netlink::rtnl_store_reply(NetlinkMessages& storage, const struct nlmsghdr *msg)
229
{
Tomek Mrugalski's avatar
Tomek Mrugalski committed
230 231 232
    // we need to make a copy of this message. We really can't allocate
    // nlmsghdr directly as it is only part of the structure. There are
    // many message types with varying lengths and a common header.
233
    struct nlmsghdr* copy = reinterpret_cast<struct nlmsghdr*>(new char[msg->nlmsg_len]);
234
    memcpy(copy, msg, msg->nlmsg_len);
235

236
    // push_back copies only pointer content, not the pointed-to object.
237
    storage.push_back(copy);
238 239
}

240 241
/// @brief Parses rtattr message.
///
242 243 244 245
/// Some netlink messages represent address information. Such messages
/// are concatenated collection of rtaddr structures. This function
/// iterates over that list and stores pointers to those messages in
/// flat array (table).
246
///
247 248 249 250
/// @param table rtattr Messages will be stored here
/// @param rta Pointer to first rtattr object
/// @param len Length (in bytes) of concatenated rtattr list.
void Netlink::parse_rtattr(RTattribPtrs& table, struct rtattr* rta, int len)
251
{
252
    std::fill(table.begin(), table.end(), static_cast<struct rtattr*>(NULL));
253 254 255 256 257 258
    // RTA_OK and RTA_NEXT() are macros defined in linux/rtnetlink.h
    // they are used to handle rtattributes. RTA_OK checks if the structure
    // pointed by rta is reasonable and passes all sanity checks.
    // RTA_NEXT() returns pointer to the next rtattr structure that
    // immediately follows pointed rta structure. See aforementioned
    // header for details.
259
    while (RTA_OK(rta, len)) {
260
        if (rta->rta_type < table.size()) {
261
            table[rta->rta_type] = rta;
262
        }
263
        rta = RTA_NEXT(rta,len);
264 265
    }
    if (len) {
266
        isc_throw(Unexpected, "Failed to parse RTATTR in netlink message.");
267 268 269
    }
}

270 271
/// @brief Parses addr_info and appends appropriate addresses to Iface object.
///
272
/// Netlink is a fine, but convoluted interface. It returns a concatenated
273 274 275 276 277
/// collection of netlink messages. Some of those messages convey information
/// about addresses. Those messages are in fact appropriate header followed
/// by concatenated lists of rtattr structures that define various pieces
/// of address information.
///
278 279
/// @param iface interface representation (addresses will be added here)
/// @param addr_info collection of parsed netlink messages
280
void Netlink::ipaddrs_get(IfaceMgr::Iface& iface, NetlinkMessages& addr_info) {
281 282
    uint8_t addr[V6ADDRESS_LEN];
    RTattribPtrs rta_tb;
283

284 285
    for (NetlinkMessages::const_iterator msg = addr_info.begin();
         msg != addr_info.end(); ++msg) {
286
        ifaddrmsg* ifa = static_cast<ifaddrmsg*>(NLMSG_DATA(*msg));
287

288
        // These are not the addresses you are looking for
289
        if (ifa->ifa_index != iface.getIndex()) {
290 291 292
            continue;
        }

293
        if ((ifa->ifa_family == AF_INET6) || (ifa->ifa_family == AF_INET)) {
294
            std::fill(rta_tb.begin(), rta_tb.end(), static_cast<rtattr*>(NULL));
295
            parse_rtattr(rta_tb, IFA_RTA(ifa), (*msg)->nlmsg_len - NLMSG_LENGTH(sizeof(*ifa)));
296 297 298 299
            if (!rta_tb[IFA_LOCAL]) {
                rta_tb[IFA_LOCAL] = rta_tb[IFA_ADDRESS];
            }
            if (!rta_tb[IFA_ADDRESS]) {
300
                rta_tb[IFA_ADDRESS] = rta_tb[IFA_LOCAL];
301
            }
302

303 304 305
            memcpy(addr, RTA_DATA(rta_tb[IFLA_ADDRESS]),
                   ifa->ifa_family==AF_INET?V4ADDRESS_LEN:V6ADDRESS_LEN);
            IOAddress a = IOAddress::from_bytes(ifa->ifa_family, addr);
306
            iface.addAddress(a);
307

308
            /// TODO: Read lifetimes of configured IPv6 addresses
309
        }
310 311 312
    }
}

313 314
/// @brief Processes reply received over netlink socket.
///
315
/// This method parses the received buffer (a collection of concatenated
316
/// netlink messages), copies each received message to newly allocated
317
/// memory and stores pointers to it in the "info" container.
318
///
319 320 321
/// @param info received netlink messages will be stored here.  It is the
///        caller's responsibility to release the memory associated with the
///        messages by calling the release_list() method.
322
void Netlink::rtnl_process_reply(NetlinkMessages& info) {
323 324 325 326
    sockaddr_nl nladdr;
    iovec iov;
    msghdr msg;
    memset(&msg, 0, sizeof(msghdr));
327 328 329 330 331
    msg.msg_name = &nladdr;
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

332
    char buf[RCVBUF_SIZE];
333

334
    iov.iov_base = buf;
335
    iov.iov_len = sizeof(buf);
336
    while (true) {
337
        int status = recvmsg(fd_, &msg, 0);
338 339

        if (status < 0) {
340
            if (errno == EINTR) {
341
                continue;
342 343 344
            }
            isc_throw(Unexpected, "Error " << errno
                      << " while processing reply from netlink socket.");
345 346 347 348 349 350
        }

        if (status == 0) {
            isc_throw(Unexpected, "EOF while reading netlink socket.");
        }

351
        nlmsghdr* header = static_cast<nlmsghdr*>(static_cast<void*>(buf));
352
        while (NLMSG_OK(header, status)) {
353

354 355 356
            // Received a message not addressed to our process, or not
            // with a sequence number we are expecting.  Ignore, and
            // look at the next one.
357
            if (nladdr.nl_pid != 0 ||
358 359
                header->nlmsg_pid != local_.nl_pid ||
                header->nlmsg_seq != dump_) {
360
                header = NLMSG_NEXT(header, status);
361 362 363
                continue;
            }

364
            if (header->nlmsg_type == NLMSG_DONE) {
365
                // End of message.
366 367 368
                return;
            }

369
            if (header->nlmsg_type == NLMSG_ERROR) {
370
                nlmsgerr* err = static_cast<nlmsgerr*>(NLMSG_DATA(header));
371
                if (header->nlmsg_len < NLMSG_LENGTH(sizeof(struct nlmsgerr))) {
372
                    // We are really out of luck here. We can't even say what is
373 374 375 376 377
                    // wrong as error message is truncated. D'oh.
                    isc_throw(Unexpected, "Netlink reply read failed.");
                } else {
                    isc_throw(Unexpected, "Netlink reply read error " << -err->error);
                }
378
                // Never happens we throw before we reach here
379 380 381 382
                return;
            }

            // store the data
383
            rtnl_store_reply(info, header);
384

385
            header = NLMSG_NEXT(header, status);
386 387 388 389 390 391 392
        }
        if (msg.msg_flags & MSG_TRUNC) {
            isc_throw(Unexpected, "Message received over netlink truncated.");
        }
        if (status) {
            isc_throw(Unexpected, "Trailing garbage of " << status << " bytes received over netlink.");
        }
393 394 395
    }
}

Tomek Mrugalski's avatar
Tomek Mrugalski committed
396
/// @brief releases nlmsg structure
397
///
398
/// @param messages Set of messages to be freed.
399
void Netlink::release_list(NetlinkMessages& messages) {
400
    // let's free local copies of stored messages
401
    for (NetlinkMessages::iterator msg = messages.begin(); msg != messages.end(); ++msg) {
402
        delete[] (*msg);
403
    }
404 405

    // ang get rid of the message pointers as well
406
    messages.clear();
407 408
}

Tomek Mrugalski's avatar
Tomek Mrugalski committed
409 410 411 412 413
} // end of anonymous namespace

namespace isc {
namespace dhcp {

414
/// @brief Detect available interfaces on Linux systems.
415
///
416 417
/// Uses the socket-based netlink protocol to retrieve the list of interfaces
/// from the Linux kernel.
418
void IfaceMgr::detectIfaces() {
419
    // Copies of netlink messages about links will be stored here.
420
    Netlink::NetlinkMessages link_info;
421 422

    // Copies of netlink messages about addresses will be stored here.
423
    Netlink::NetlinkMessages addr_info;
424

425
    // Socket descriptors and other rtnl-related parameters.
426
    Netlink nl;
427

428
    // Table with pointers to address attributes.
429
    Netlink::RTattribPtrs attribs_table;
430 431
    std::fill(attribs_table.begin(), attribs_table.end(),
              static_cast<struct rtattr*>(NULL));
432

433
    // Open socket
434
    nl.rtnl_open_socket();
435

436 437
    // Now we have open functional socket, let's use it!
    // Ask for list of network interfaces...
438
    nl.rtnl_send_request(AF_PACKET, RTM_GETLINK);
439

440 441 442 443 444 445 446
    // Get reply and store it in link_info list:
    // response is received as with any other socket - just a series
    // of bytes. They are representing collection of netlink messages
    // concatenated together. rtnl_process_reply will parse this
    // buffer, copy each message to a newly allocated memory and
    // store pointers to it in link_info. This allocated memory will
    // be released later. See release_info(link_info) below.
447
    nl.rtnl_process_reply(link_info);
448 449

    // Now ask for list of addresses (AF_UNSPEC = of any family)
450 451 452
    // Let's repeat, but this time ask for any addresses.
    // That includes IPv4, IPv6 and any other address families that
    // are happen to be supported by this system.
453
    nl.rtnl_send_request(AF_UNSPEC, RTM_GETADDR);
454

455
    // Get reply and store it in addr_info list.
456 457
    // Again, we will allocate new memory and store messages in
    // addr_info. It will be released later using release_info(addr_info).
458
    nl.rtnl_process_reply(addr_info);
459 460

    // Now build list with interface names
461 462
    for (Netlink::NetlinkMessages::iterator msg = link_info.begin();
         msg != link_info.end(); ++msg) {
463
        // Required to display information about interface
464
        struct ifinfomsg* interface_info = static_cast<ifinfomsg*>(NLMSG_DATA(*msg));
465
        int len = (*msg)->nlmsg_len;
466
        len -= NLMSG_LENGTH(sizeof(*interface_info));
467
        nl.parse_rtattr(attribs_table, IFLA_RTA(interface_info), len);
468

469 470 471
        // valgrind reports *possible* memory leak in the line below, but it is
        // bogus. Nevertheless, the whole interface definition has been split
        // into three separate steps for easier debugging.
472
        const char* tmp = static_cast<const char*>(RTA_DATA(attribs_table[IFLA_IFNAME]));
473
        string iface_name(tmp); // <--- bogus valgrind warning here
474
        Iface iface = Iface(iface_name, interface_info->ifi_index);
475

476
        iface.setHWType(interface_info->ifi_type);
477
        iface.setFlags(interface_info->ifi_flags);
478

479
        // Does inetface have LL_ADDR?
480
        if (attribs_table[IFLA_ADDRESS]) {
481 482
            iface.setMac(static_cast<const uint8_t*>(RTA_DATA(attribs_table[IFLA_ADDRESS])),
                         RTA_PAYLOAD(attribs_table[IFLA_ADDRESS]));
483 484
        }
        else {
485 486
            // Tunnels can have no LL_ADDR. RTA_PAYLOAD doesn't check it and
            // try to dereference it in this manner
487 488
        }

489
        nl.ipaddrs_get(iface, addr_info);
490
        ifaces_.push_back(iface);
491
    }
492

493 494
    nl.release_list(link_info);
    nl.release_list(addr_info);
495 496
}

497 498 499 500 501 502
/// @brief sets flag_*_ fields.
///
/// This implementation is OS-specific as bits have different meaning
/// on different OSes.
///
/// @param flags flags bitfield read from OS
503 504 505 506 507 508 509 510 511 512
void IfaceMgr::Iface::setFlags(uint32_t flags) {
    flags_ = flags;

    flag_loopback_ = flags & IFF_LOOPBACK;
    flag_up_ = flags & IFF_UP;
    flag_running_ = flags & IFF_RUNNING;
    flag_multicast_ = flags & IFF_MULTICAST;
    flag_broadcast_ = flags & IFF_BROADCAST;
}

513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
void IfaceMgr::os_send4(struct msghdr& m, boost::scoped_array<char>& control_buf,
                        size_t control_buf_len, const Pkt4Ptr& pkt) {

    // Setting the interface is a bit more involved.
    //
    // We have to create a "control message", and set that to
    // define the IPv4 packet information. We could set the
    // source address if we wanted, but we can safely let the
    // kernel decide what that should be.
    m.msg_control = &control_buf[0];
    m.msg_controllen = control_buf_len;
    struct cmsghdr* cmsg = CMSG_FIRSTHDR(&m);
    cmsg->cmsg_level = IPPROTO_IP;
    cmsg->cmsg_type = IP_PKTINFO;
    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();
    m.msg_controllen = cmsg->cmsg_len;
}

bool IfaceMgr::os_receive4(struct msghdr& m, Pkt4Ptr& pkt) {
    struct cmsghdr* cmsg;
    struct in_pktinfo* pktinfo;
    struct in_addr to_addr;

    memset(&to_addr, 0, sizeof(to_addr));

    cmsg = CMSG_FIRSTHDR(&m);
    while (cmsg != NULL) {
        if ((cmsg->cmsg_level == IPPROTO_IP) &&
            (cmsg->cmsg_type == IP_PKTINFO)) {
            pktinfo = (struct in_pktinfo*)CMSG_DATA(cmsg);
546

547 548 549 550 551 552 553 554 555
            pkt->setIndex(pktinfo->ipi_ifindex);
            pkt->setLocalAddr(IOAddress(htonl(pktinfo->ipi_addr.s_addr)));
            return (true);

            // This field is useful, when we are bound to unicast
            // address e.g. 192.0.2.1 and the packet was sent to
            // broadcast. This will return broadcast address, not
            // the address we are bound to.

556
            // XXX: Perhaps we should uncomment this:
557 558 559 560 561 562 563 564
            // to_addr = pktinfo->ipi_spec_dst;
        }
        cmsg = CMSG_NXTHDR(&m, cmsg);
    }

    return (false);
}

565
} // end of isc::dhcp namespace
566
} // end of isc namespace
567 568

#endif // if defined(LINUX)