| /* |
| * |
| * Ethernet daemon for Linux |
| * |
| * Copyright (C) 2017-2019 Intel Corporation. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <stdio.h> |
| #include <errno.h> |
| #include <unistd.h> |
| #include <fnmatch.h> |
| #include <net/if_arp.h> |
| #include <linux/rtnetlink.h> |
| #include <linux/if.h> |
| #include <linux/if_ether.h> |
| #include <linux/if_packet.h> |
| #include <linux/filter.h> |
| #include <arpa/inet.h> |
| #include <ell/ell.h> |
| |
| #include "src/module.h" |
| #include "src/eap.h" |
| #include "wired/dbus.h" |
| #include "wired/network.h" |
| #include "wired/ethdev.h" |
| |
| #define ADAPTER_INTERFACE "net.connman.ead.Adapter" |
| #define ADAPTER_BASEPATH EAD_BASE_PATH "/adapter" |
| |
| #define PROP_NAME "Name" |
| #define PROP_ADDRESS "Address" |
| #define PROP_ACTIVE "Active" |
| #define PROP_CONNECTED "Connected" |
| #define PROP_AUTHENTICATED "Authenticated" |
| |
| struct ethdev { |
| uint32_t index; |
| char ifname[IFNAMSIZ]; |
| uint8_t addr[ETH_ALEN]; |
| bool active; |
| bool lower_up; |
| bool auth_done; |
| struct l_queue *eapol_sessions; |
| char *path; |
| }; |
| |
| struct eapol { |
| struct ethdev *dev; |
| uint8_t addr[ETH_ALEN]; |
| struct eap_state *eap; |
| struct l_settings *cred; |
| }; |
| |
| static struct l_netlink *rtnl = NULL; |
| static struct l_queue *ethdev_list = NULL; |
| static char **whitelist_filter = NULL; |
| static char **blacklist_filter = NULL; |
| |
| static struct l_io *pae_io; |
| |
| struct eapol_hdr { |
| uint8_t proto_ver; |
| uint8_t pkt_type; |
| __be16 pkt_len; |
| } __attribute__ ((packed)); |
| |
| static const uint8_t eapol_start[] = { 0x01, 0x01, 0x00, 0x00 }; |
| static const uint8_t pae_group_addr[] = { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03 }; |
| |
| static bool pae_write(struct ethdev *dev, const uint8_t *addr, |
| const uint8_t *frame, size_t len) |
| { |
| int fd = l_io_get_fd(pae_io); |
| struct sockaddr_ll sll; |
| ssize_t res; |
| |
| memset(&sll, 0, sizeof(sll)); |
| sll.sll_family = AF_PACKET; |
| sll.sll_ifindex = dev->index; |
| sll.sll_protocol = htons(ETH_P_PAE); |
| sll.sll_halen = ETH_ALEN; |
| memcpy(sll.sll_addr, addr, ETH_ALEN); |
| |
| res = sendto(fd, frame, len, 0, |
| (struct sockaddr *) &sll, sizeof(sll)); |
| if (res < 0) |
| return false; |
| |
| return true; |
| } |
| |
| static void eapol_free(void *data) |
| { |
| struct eapol *eapol = data; |
| |
| l_debug("Freeing EAPoL session"); |
| |
| eap_free(eapol->eap); |
| l_settings_free(eapol->cred); |
| l_free(eapol); |
| } |
| |
| static bool eapol_match(const void *a, const void *b) |
| { |
| const struct eapol *eapol = a; |
| |
| return !memcmp(eapol->addr, b, ETH_ALEN); |
| } |
| |
| static struct eapol *eapol_lookup(struct ethdev *dev, const uint8_t *addr) |
| { |
| return l_queue_find(dev->eapol_sessions, eapol_match, addr); |
| } |
| |
| static bool ethdev_match(const void *a, const void *b) |
| { |
| const struct ethdev *dev = a; |
| uint32_t index = L_PTR_TO_UINT(b); |
| |
| return (dev->index == index); |
| } |
| |
| static struct ethdev *ethdev_lookup(uint32_t index) |
| { |
| return l_queue_find(ethdev_list, ethdev_match, L_UINT_TO_PTR(index)); |
| } |
| |
| static void eap_tx_packet(const uint8_t *eap_data, size_t len, void *user_data) |
| { |
| struct eapol *eapol = user_data; |
| uint8_t frame[1500]; |
| |
| l_put_u8(0x01, frame); |
| l_put_u8(0x00, frame + 1); |
| l_put_be16(len, frame + 2); |
| memcpy(frame + 4, eap_data, len); |
| |
| /* |
| * The supplicant / client always uses the PAE group address for |
| * sending the EAP packets. |
| */ |
| pae_write(eapol->dev, pae_group_addr, frame, len + 4); |
| } |
| |
| static void eap_complete(enum eap_result result, void *user_data) |
| { |
| struct eapol *eapol = user_data; |
| struct ethdev *dev = eapol->dev; |
| |
| l_debug("result %u", result); |
| |
| if (result == EAP_RESULT_SUCCESS) { |
| if (!dev->auth_done) { |
| dev->auth_done = true; |
| l_dbus_property_changed(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE, |
| PROP_AUTHENTICATED); |
| } |
| } |
| |
| l_queue_remove(dev->eapol_sessions, eapol); |
| eapol_free(eapol); |
| } |
| |
| static void eap_key_material(const uint8_t *msk_data, size_t msk_len, |
| const uint8_t *emsk_data, size_t emsk_len, |
| const uint8_t *iv, size_t iv_len, |
| const uint8_t *session_id, size_t session_len, |
| void *user_data) |
| { |
| l_debug("EAP key material received"); |
| } |
| |
| static void eap_event(unsigned int event, const void *event_data, |
| void *user_data) |
| { |
| l_debug("event %u", event); |
| } |
| |
| static void rx_packet(struct ethdev *dev, const uint8_t *addr, |
| const void *frame, size_t len) |
| { |
| const struct eapol_hdr *hdr = frame; |
| struct eapol *eapol; |
| uint16_t pkt_len; |
| |
| if (len < 4) { |
| l_error("Too short EAPoL packet with %zu bytes", len); |
| return; |
| } |
| |
| pkt_len = L_BE16_TO_CPU(hdr->pkt_len); |
| |
| /* |
| * EAPoL packet frames might contain padding at the end and so just |
| * ensure that at least packet body length worth of packet body is |
| * actually present. |
| */ |
| if (len - 4 < pkt_len) { |
| l_error("Missing %zu bytes from EAPoL packet", |
| pkt_len - (len - 4)); |
| return; |
| } |
| |
| switch (hdr->pkt_type) { |
| case 0x00: /* EAP-Packet */ |
| eapol = eapol_lookup(dev, addr); |
| if (!eapol) { |
| eapol = l_new(struct eapol, 1); |
| eapol->dev = dev; |
| memcpy(eapol->addr, addr, ETH_ALEN); |
| eapol->eap = eap_new(eap_tx_packet, |
| eap_complete, eapol); |
| if (!eapol->eap) { |
| l_error("Failed to create EAP instance"); |
| l_free(eapol); |
| return; |
| } |
| |
| l_debug("Created new EAPoL session"); |
| |
| l_queue_push_tail(dev->eapol_sessions, eapol); |
| |
| eapol->cred = network_lookup_security("default"); |
| eap_load_settings(eapol->eap, eapol->cred, "EAP-"); |
| |
| eap_set_key_material_func(eapol->eap, eap_key_material); |
| eap_set_event_func(eapol->eap, eap_event); |
| } |
| eap_rx_packet(eapol->eap, frame + 4, pkt_len); |
| break; |
| } |
| } |
| |
| /* |
| * BPF filter to match skb->dev->type == 1 (ARPHRD_ETHER) and |
| * match skb->protocol == 0x888e (PAE). |
| */ |
| static struct sock_filter pae_filter[] = { |
| { 0x28, 0, 0, 0xfffff01c }, /* ldh #hatype */ |
| { 0x15, 0, 3, 0x00000001 }, /* jne #1, drop */ |
| { 0x28, 0, 0, 0xfffff000 }, /* ldh #proto */ |
| { 0x15, 0, 1, 0x0000888e }, /* jne #0x888e, drop */ |
| { 0x06, 0, 0, 0xffffffff }, /* keep: ret #-1 */ |
| { 0x06, 0, 0, 0000000000 }, /* drop: ret #0 */ |
| }; |
| |
| static const struct sock_fprog pae_fprog = { .len = 6, .filter = pae_filter }; |
| |
| static bool pae_read(struct l_io *io, void *user_data) |
| { |
| int fd = l_io_get_fd(io); |
| struct ethdev *dev; |
| struct sockaddr_ll sll; |
| socklen_t sll_len; |
| ssize_t bytes; |
| uint8_t frame[1500]; |
| |
| memset(&sll, 0, sizeof(sll)); |
| sll_len = sizeof(sll); |
| |
| bytes = recvfrom(fd, frame, sizeof(frame), 0, |
| (struct sockaddr *) &sll, &sll_len); |
| if (bytes <= 0) { |
| l_error("Reading from PAE socket failed: %s", strerror(errno)); |
| return false; |
| } |
| |
| if (sll.sll_hatype != ARPHRD_ETHER) |
| return true; |
| |
| if (sll.sll_halen != ETH_ALEN) |
| return true; |
| |
| if (ntohs(sll.sll_protocol) != ETH_P_PAE) |
| return true; |
| |
| if (sll.sll_pkttype != PACKET_HOST && |
| sll.sll_pkttype != PACKET_MULTICAST) |
| return true; |
| |
| dev = ethdev_lookup(sll.sll_ifindex); |
| if (!dev) |
| return true; |
| |
| rx_packet(dev, sll.sll_addr, frame, bytes); |
| |
| return true; |
| } |
| |
| static void pae_destroy() |
| { |
| pae_io = NULL; |
| } |
| |
| static bool pae_open(void) |
| { |
| int fd; |
| |
| fd = socket(PF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, |
| htons(ETH_P_ALL)); |
| if (fd < 0) |
| return false; |
| |
| if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, |
| &pae_fprog, sizeof(pae_fprog)) < 0) { |
| close(fd); |
| return false; |
| } |
| |
| pae_io = l_io_new(fd); |
| l_io_set_close_on_destroy(pae_io, true); |
| |
| l_debug("Opened PAE socket"); |
| |
| l_io_set_read_handler(pae_io, pae_read, NULL, pae_destroy); |
| |
| return true; |
| } |
| |
| static void pae_close(void) |
| { |
| l_io_destroy(pae_io); |
| pae_io = NULL; |
| |
| l_debug("Closed PAE socket"); |
| } |
| |
| static void do_debug(const char *str, void *user_data) |
| { |
| const char *prefix = user_data; |
| |
| l_info("%s%s", prefix, str); |
| } |
| |
| static char *read_devtype_from_uevent(const char *ifname) |
| { |
| char line[128], *filename, *devtype = NULL; |
| FILE *f; |
| |
| if (!ifname) |
| return NULL; |
| |
| filename = l_strdup_printf("/sys/class/net/%s/uevent", ifname); |
| f = fopen(filename, "re"); |
| l_free(filename); |
| |
| if (!f) |
| return NULL; |
| |
| while (fgets(line, sizeof(line), f)) { |
| char *pos; |
| |
| pos = strchr(line, '\n'); |
| if (!pos) |
| continue; |
| pos[0] = '\0'; |
| |
| if (!strncmp(line, "DEVTYPE=", 8)) { |
| devtype = l_strdup(line + 8); |
| break; |
| } |
| } |
| |
| fclose(f); |
| |
| return devtype; |
| } |
| |
| static int modify_membership(struct ethdev *dev, int optname) |
| { |
| struct packet_mreq mreq; |
| int fd; |
| |
| fd = l_io_get_fd(pae_io); |
| if (fd < 0) |
| return -1; |
| |
| memset(&mreq, 0, sizeof(mreq)); |
| mreq.mr_ifindex = dev->index; |
| mreq.mr_type = PACKET_MR_MULTICAST; |
| mreq.mr_alen = ETH_ALEN; |
| memcpy(mreq.mr_address, pae_group_addr, ETH_ALEN); |
| |
| return setsockopt(fd, SOL_PACKET, optname, &mreq, sizeof(mreq)); |
| } |
| |
| static void ethdev_free(void *data) |
| { |
| struct ethdev *dev = data; |
| |
| l_debug("Freeing device %s", dev->ifname); |
| |
| modify_membership(dev, PACKET_DROP_MEMBERSHIP); |
| |
| l_queue_destroy(dev->eapol_sessions, eapol_free); |
| |
| l_dbus_object_remove_interface(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE); |
| |
| l_free(dev->path); |
| l_free(dev); |
| } |
| |
| static bool is_ifname_valid(const char *ifname) |
| { |
| char *pattern; |
| unsigned int i; |
| |
| if (!whitelist_filter) |
| goto check_blacklist; |
| |
| for (i = 0; (pattern = whitelist_filter[i]); i++) { |
| if (fnmatch(pattern, ifname, 0)) |
| continue; |
| |
| goto check_blacklist; |
| } |
| |
| return false; |
| |
| check_blacklist: |
| if (!blacklist_filter) |
| return true; |
| |
| for (i = 0; (pattern = blacklist_filter[i]); i++) { |
| if (!fnmatch(pattern, ifname, 0)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static void newlink_notify(const struct ifinfomsg *ifi, int bytes) |
| { |
| uint32_t index = ifi->ifi_index; |
| struct ethdev *dev; |
| const struct rtattr *attr; |
| uint8_t *addr = NULL; |
| const char *ifname = NULL; |
| uint8_t linkmode = 0, operstate = 0; |
| bool active, lower_up, lower_changed = false; |
| |
| if (ifi->ifi_type != ARPHRD_ETHER) |
| return; |
| |
| active = ifi->ifi_flags & IFF_UP; |
| lower_up = ifi->ifi_flags & IFF_LOWER_UP; |
| |
| for (attr = IFLA_RTA(ifi); RTA_OK(attr, bytes); |
| attr = RTA_NEXT(attr, bytes)) { |
| switch (attr->rta_type) { |
| case IFLA_ADDRESS: |
| if (RTA_PAYLOAD(attr) == ETH_ALEN) |
| addr = RTA_DATA(attr); |
| break; |
| case IFLA_IFNAME: |
| ifname = RTA_DATA(attr); |
| break; |
| case IFLA_LINKMODE: |
| linkmode = l_get_u8(RTA_DATA(attr)); |
| break; |
| case IFLA_OPERSTATE: |
| operstate = l_get_u8(RTA_DATA(attr)); |
| break; |
| } |
| } |
| |
| if (!addr || !ifname) |
| return; |
| |
| l_debug("%s: linkmode %u operstate %u", ifname, linkmode, operstate); |
| |
| if (!is_ifname_valid(ifname)) { |
| l_debug("Ignoring device with interface name %s", ifname); |
| return; |
| } |
| |
| dev = ethdev_lookup(index); |
| if (!dev) { |
| char *devtype; |
| |
| /* |
| * If there is no existing Ethernet device structure, then |
| * first check uevent if this is wired Ethernet or not. |
| */ |
| devtype = read_devtype_from_uevent(ifname); |
| if (devtype) { |
| l_free(devtype); |
| return; |
| } |
| |
| if (l_queue_isempty(ethdev_list)) { |
| if (!pae_open()) { |
| l_error("Failed to open PAE port"); |
| return; |
| } |
| } |
| |
| dev = l_new(struct ethdev, 1); |
| dev->index = index; |
| dev->active = active; |
| dev->lower_up = lower_up; |
| dev->auth_done = false; |
| dev->eapol_sessions = l_queue_new(); |
| dev->path = l_strdup_printf("%s/%u", ADAPTER_BASEPATH, |
| dev->index); |
| |
| l_debug("Creating device %u", dev->index); |
| |
| modify_membership(dev, PACKET_ADD_MEMBERSHIP); |
| |
| l_dbus_object_add_interface(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE, dev); |
| |
| l_dbus_object_add_interface(dbus_app_get(), dev->path, |
| L_DBUS_INTERFACE_PROPERTIES, NULL); |
| |
| l_queue_push_tail(ethdev_list, dev); |
| |
| lower_changed = true; |
| } |
| |
| if (ifname) |
| strcpy(dev->ifname, ifname); |
| |
| memcpy(dev->addr, addr, ETH_ALEN); |
| |
| if (active != dev->active) { |
| dev->active = active; |
| l_dbus_property_changed(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE, PROP_ACTIVE); |
| } |
| |
| if (lower_up != dev->lower_up) { |
| if (!lower_up) { |
| dev->auth_done = false; |
| l_dbus_property_changed(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE, PROP_AUTHENTICATED); |
| } |
| |
| dev->lower_up = lower_up; |
| l_dbus_property_changed(dbus_app_get(), dev->path, |
| ADAPTER_INTERFACE, PROP_CONNECTED); |
| |
| lower_changed = true; |
| } |
| |
| if (lower_changed) { |
| if (dev->lower_up) |
| pae_write(dev, pae_group_addr, |
| eapol_start, sizeof(eapol_start)); |
| else |
| l_queue_clear(dev->eapol_sessions, eapol_free); |
| } |
| } |
| |
| static void dellink_notify(const struct ifinfomsg *ifi, int bytes) |
| { |
| uint32_t index = ifi->ifi_index; |
| struct ethdev *dev; |
| |
| if (ifi->ifi_type != ARPHRD_ETHER) |
| return; |
| |
| dev = l_queue_remove_if(ethdev_list, ethdev_match, |
| L_UINT_TO_PTR(index)); |
| if (!dev) |
| return; |
| |
| l_debug("Removing device %u", dev->index); |
| |
| ethdev_free(dev); |
| |
| if (l_queue_isempty(ethdev_list)) |
| pae_close(); |
| } |
| |
| static void link_notify(uint16_t type, const void *data, uint32_t len, |
| void *user_data) |
| { |
| const struct ifinfomsg *ifi = data; |
| unsigned int bytes; |
| |
| if (ifi->ifi_type != ARPHRD_ETHER) |
| return; |
| |
| bytes = len - NLMSG_ALIGN(sizeof(struct ifinfomsg)); |
| |
| switch (type) { |
| case RTM_NEWLINK: |
| newlink_notify(ifi, bytes); |
| break; |
| case RTM_DELLINK: |
| dellink_notify(ifi, bytes); |
| break; |
| } |
| } |
| |
| static void getlink_callback(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| const struct ifinfomsg *ifi = data; |
| unsigned int bytes; |
| |
| if (error) { |
| l_error("Failure with link information dump (%d)", error); |
| return; |
| } |
| |
| if (type != RTM_NEWLINK) |
| return; |
| |
| bytes = len - NLMSG_ALIGN(sizeof(struct ifinfomsg)); |
| |
| newlink_notify(ifi, bytes); |
| } |
| |
| static bool property_get_name(struct l_dbus *dbus, |
| struct l_dbus_message *message, |
| struct l_dbus_message_builder *builder, |
| void *user_data) |
| { |
| struct ethdev *dev = user_data; |
| |
| l_dbus_message_builder_append_basic(builder, 's', dev->ifname); |
| return true; |
| } |
| |
| static bool property_get_address(struct l_dbus *dbus, |
| struct l_dbus_message *message, |
| struct l_dbus_message_builder *builder, |
| void *user_data) |
| { |
| struct ethdev *dev = user_data; |
| char str[18]; |
| |
| sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x", |
| dev->addr[0], dev->addr[1], dev->addr[2], |
| dev->addr[3], dev->addr[4], dev->addr[5]); |
| |
| l_dbus_message_builder_append_basic(builder, 's', str); |
| return true; |
| } |
| |
| static bool property_get_active(struct l_dbus *dbus, |
| struct l_dbus_message *message, |
| struct l_dbus_message_builder *builder, |
| void *user_data) |
| { |
| struct ethdev *dev = user_data; |
| bool active = dev->active; |
| |
| l_dbus_message_builder_append_basic(builder, 'b', &active); |
| return true; |
| } |
| |
| static bool property_get_connected(struct l_dbus *dbus, |
| struct l_dbus_message *message, |
| struct l_dbus_message_builder *builder, |
| void *user_data) |
| { |
| struct ethdev *dev = user_data; |
| bool connected = dev->lower_up; |
| |
| l_dbus_message_builder_append_basic(builder, 'b', &connected); |
| return true; |
| } |
| |
| static bool property_get_authenticated(struct l_dbus *dbus, |
| struct l_dbus_message *message, |
| struct l_dbus_message_builder *builder, |
| void *user_data) |
| { |
| struct ethdev *dev = user_data; |
| bool authenticated = dev->auth_done; |
| |
| l_dbus_message_builder_append_basic(builder, 'b', &authenticated); |
| return true; |
| } |
| |
| static void setup_adapter_interface(struct l_dbus_interface *interface) |
| { |
| l_dbus_interface_property(interface, PROP_NAME, 0, "s", |
| property_get_name, NULL); |
| l_dbus_interface_property(interface, PROP_ADDRESS, 0, "s", |
| property_get_address, NULL); |
| l_dbus_interface_property(interface, PROP_ACTIVE, 0, "b", |
| property_get_active, NULL); |
| l_dbus_interface_property(interface, PROP_CONNECTED, 0, "b", |
| property_get_connected, NULL); |
| l_dbus_interface_property(interface, PROP_AUTHENTICATED, 0, "b", |
| property_get_authenticated, NULL); |
| } |
| |
| bool ethdev_init(const char *whitelist, const char *blacklist) |
| { |
| struct ifinfomsg msg; |
| |
| if (rtnl) |
| return false; |
| |
| l_debug("Opening route netlink socket"); |
| |
| rtnl = l_netlink_new(NETLINK_ROUTE); |
| if (!rtnl) { |
| l_error("Failed to open route netlink socket"); |
| return false; |
| } |
| |
| if (getenv("EAD_RTNL_DEBUG")) |
| l_netlink_set_debug(rtnl, do_debug, "[RTNL] ", NULL); |
| |
| if (!l_netlink_register(rtnl, RTNLGRP_LINK, link_notify, NULL, NULL)) { |
| l_error("Failed to register for RTNL link notifications"); |
| l_netlink_destroy(rtnl); |
| return false; |
| } |
| |
| ethdev_list = l_queue_new(); |
| |
| if (!l_dbus_register_interface(dbus_app_get(), ADAPTER_INTERFACE, |
| setup_adapter_interface, NULL, false)) { |
| l_error("Unable to register the adapter interface"); |
| l_netlink_destroy(rtnl); |
| return false; |
| } |
| |
| if (whitelist) |
| whitelist_filter = l_strsplit(whitelist, ','); |
| |
| if (blacklist) |
| blacklist_filter = l_strsplit(blacklist, ','); |
| |
| memset(&msg, 0, sizeof(msg)); |
| |
| l_netlink_send(rtnl, RTM_GETLINK, NLM_F_DUMP, &msg, sizeof(msg), |
| getlink_callback, NULL, NULL); |
| |
| return true; |
| } |
| |
| void ethdev_exit(void) |
| { |
| if (!rtnl) |
| return; |
| |
| l_dbus_unregister_interface(dbus_app_get(), ADAPTER_INTERFACE); |
| |
| pae_close(); |
| |
| l_strfreev(whitelist_filter); |
| l_strfreev(blacklist_filter); |
| |
| l_debug("Closing route netlink socket"); |
| l_netlink_destroy(rtnl); |
| rtnl = NULL; |
| |
| l_queue_destroy(ethdev_list, ethdev_free); |
| ethdev_list = NULL; |
| } |