blob: e000afae7eb9870d4961be54facbd6821fbb9bd0 [file] [log] [blame]
/*
*
* 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;
}