dhcp: Port the plugin to FreeBSD/macOS

This also refactors the BPF handling so it can be shared between the
dhcp and farp plugins.  The latter is adapted accordingly.

Closes strongswan/strongswan#2047

Co-authored-by: Tobias Brunner <[email protected]>
This commit is contained in:
Dan James
2024-02-19 09:17:53 +01:00
committed by Tobias Brunner
co-authored by Tobias Brunner
parent 10a876d54c
commit 187c72d1af
6 changed files with 973 additions and 615 deletions
+649
View File
@@ -0,0 +1,649 @@
/*
* Copyright (C) 2021-2024 Tobias Brunner
* Copyright (C) 2020-2023 Dan James <[email protected]>
* Copyright (C) 2010 Martin Willi
*
* Copyright (C) secunet Security Networks AG
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
*
* This program 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 General Public License
* for more details.
*/
#include "pf_handler.h"
#include <library.h>
#include <unistd.h>
#if !defined(__APPLE__) && !defined(__FreeBSD__)
#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/filter.h>
#else
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/bpf.h>
#include <net/if.h>
#include <net/if_dl.h>
#endif /* !defined(__APPLE__) && !defined(__FreeBSD__) */
#include <errno.h>
#include <net/ethernet.h>
#include <sys/ioctl.h>
#if !defined(__APPLE__) && !defined(__FreeBSD__)
/**
* Number of interfaces to cache in LFU cache
*/
#define IFACE_CACHE_SIZE 8
/**
* Data for a cached interface on which packets were received
*/
struct cached_iface_t {
/**
* Index of the interface
*/
int if_index;
/**
* Name of the interface
*/
char if_name[IFNAMSIZ];
/**
* Hardware (mac) address of the interface
*/
u_char hwaddr[ETHER_ADDR_LEN];
/**
* Number of the times this info has been used, for LFU cache
*/
int used;
};
typedef struct cached_iface_t cached_iface_t;
#endif
typedef struct private_pf_handler_t private_pf_handler_t;
struct private_pf_handler_t {
/**
* Public interface
*/
pf_handler_t public;
/**
* Name for this handler
*/
const char *name;
/**
* Registered callback
*/
pf_packet_handler_t handler;
/**
* Context to pass to callback
*/
void *ctx;
#if !defined(__APPLE__) && !defined(__FreeBSD__)
/**
* AF_PACKET receive socket
*/
int receive;
/**
* Cache of frequently used interface information
*/
cached_iface_t ifaces[IFACE_CACHE_SIZE];
/**
* Number of currently cached interface information
*/
int cached;
#else
/**
* BPF sockets (one per interface), pf_socket_t
*/
linked_list_t *pf_sockets;
#endif /* !defined(__APPLE__) && !defined(__FreeBSD__) */
};
#if !defined(__APPLE__) && !defined(__FreeBSD__)
/**
* Find the index of the slot that was least frequently used
*/
static int find_least_used_cache_entry(private_pf_handler_t *this)
{
int i, idx = 0, least_used = 0;
if (this->cached < IFACE_CACHE_SIZE)
{
/* not all slots used, choose the next unused slot */
idx = this->cached++;
}
else
{
/* all slots in use, choose the one with the lowest usage */
for (i = 0; i < this->cached; i++)
{
if (this->ifaces[i].used < least_used)
{
idx = i;
least_used = this->ifaces[i].used;
}
}
}
return idx;
}
/**
* Retrieve information about the interface on which a packet was received
*/
static cached_iface_t *find_interface(private_pf_handler_t *this, int fd,
int ifindex)
{
struct ifreq req = {
.ifr_ifindex = ifindex,
};
int idx;
for (idx = 0; idx < this->cached; idx++)
{
if (this->ifaces[idx].if_index == ifindex)
{
this->ifaces[idx].used++;
return &this->ifaces[idx];
}
}
if (ioctl(fd, SIOCGIFNAME, &req) == 0 &&
ioctl(fd, SIOCGIFHWADDR, &req) == 0 &&
req.ifr_hwaddr.sa_family == ARPHRD_ETHER)
{
idx = find_least_used_cache_entry(this);
this->ifaces[idx].if_index = ifindex;
memcpy(this->ifaces[idx].if_name, req.ifr_name, IFNAMSIZ);
memcpy(this->ifaces[idx].hwaddr, req.ifr_hwaddr.sa_data, ETHER_ADDR_LEN);
this->ifaces[idx].used = 1;
return &this->ifaces[idx];
}
return NULL;
}
CALLBACK(receive_packet, bool,
private_pf_handler_t *this, int fd, watcher_event_t event)
{
cached_iface_t *iface;
struct sockaddr_ll addr;
socklen_t addr_len = sizeof(addr);
uint8_t packet[1500];
ssize_t len;
len = recvfrom(fd, &packet, sizeof(packet), MSG_DONTWAIT,
(struct sockaddr*)&addr, &addr_len);
if (len >= 0)
{
iface = find_interface(this, fd, addr.sll_ifindex);
if (iface)
{
this->handler(this->ctx, iface->if_name, iface->if_index,
chunk_create(iface->hwaddr, ETHER_ADDR_LEN), fd,
chunk_create(packet, len));
}
}
return TRUE;
}
METHOD(pf_handler_t, destroy, void,
private_pf_handler_t *this)
{
if (this->receive >= 0)
{
lib->watcher->remove(lib->watcher, this->receive);
close(this->receive);
free(this);
}
}
/**
* Setup capturing via AF_PACKET socket
*/
static bool setup_internal(private_pf_handler_t *this, char *iface,
struct sock_fprog *packet_filter)
{
int protocol = strcmp(this->name, "ARP") ? ETH_P_IP : ETH_P_ARP;
this->receive = socket(AF_PACKET, SOCK_DGRAM, htons(protocol));
if (this->receive == -1)
{
DBG1(DBG_NET, "opening %s packet socket failed: %s", this->name,
strerror(errno));
return FALSE;
}
if (setsockopt(this->receive, SOL_SOCKET, SO_ATTACH_FILTER, packet_filter,
sizeof(struct sock_fprog)) < 0)
{
DBG1(DBG_NET, "installing %s packet socket filter failed: %s",
this->name, strerror(errno));
return FALSE;
}
if (iface && !bind_to_device(this->receive, iface))
{
return FALSE;
}
lib->watcher->add(lib->watcher, this->receive, WATCHER_READ,
receive_packet, this);
DBG2(DBG_NET, "listening for %s (protocol=0x%04x) requests on fd=%d",
this->name, protocol, this->receive);
return TRUE;
}
/*
* Described in header
*/
bool bind_to_device(int fd, char *iface)
{
int status;
struct ifreq ifreq = {};
if (strlen(iface) > sizeof(ifreq.ifr_name))
{
DBG1(DBG_CFG, "name for interface too long: '%s'", iface);
return FALSE;
}
memcpy(ifreq.ifr_name, iface, sizeof(ifreq.ifr_name));
status = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifreq, sizeof(ifreq));
if (status)
{
DBG1(DBG_CFG, "binding socket to '%s' failed: %s",
iface, strerror(errno));
return FALSE;
}
return TRUE;
}
#else /* !defined(__APPLE__) && !defined(__FreeBSD__) */
/**
* A BPF socket is required for each interface.
*/
struct pf_socket_t {
/**
* Reference to the private packet filter handler
*/
private_pf_handler_t *this;
/**
* The name of the interface
*/
char *if_name;
/**
* Index of the interface
*/
int if_index;
/**
* The Ethernet MAC address of the interface
*/
chunk_t mac;
/**
* The IPv4 address of the interface
*/
host_t *ipv4;
/**
* The BPF file descriptor for this interface
*/
int fd;
/**
* The BPF packet buffer length as read from the BPF fd
*/
size_t buflen;
/**
* An allocated buffer for receiving packets from BPF
*/
uint8_t *bufdat;
};
typedef struct pf_socket_t pf_socket_t;
/**
* Free resources used by a socket.
*/
CALLBACK(destroy_pf_socket, void,
pf_socket_t *socket)
{
if (socket->fd >= 0)
{
lib->watcher->remove(lib->watcher, socket->fd);
close(socket->fd);
}
DESTROY_IF(socket->ipv4);
chunk_free(&socket->mac);
free(socket->bufdat);
free(socket->if_name);
free(socket);
}
/**
* Find the handler for the named interface, creating one if needed.
*/
static pf_socket_t *get_pf_socket(private_pf_handler_t *this, char *if_name)
{
pf_socket_t *socket, *found = NULL;
enumerator_t *enumerator;
enumerator = this->pf_sockets->create_enumerator(this->pf_sockets);
while (enumerator->enumerate(enumerator, &socket))
{
if (streq(socket->if_name, if_name))
{
found = socket;
break;
}
}
enumerator->destroy(enumerator);
if (!found)
{
INIT(found,
.this = this,
.if_name = strdup(if_name),
.fd = -1,
);
this->pf_sockets->insert_last(this->pf_sockets, found);
}
return found;
}
/**
* Find and open an available BPF device.
*/
static int bpf_open()
{
static int no_cloning_bpf = 0;
/* enough space for: /dev/bpf000\0 */
char device[12];
int n = no_cloning_bpf ? 0 : -1;
int fd;
do
{
if (n < 0)
{
snprintf(device, sizeof(device), "/dev/bpf");
}
else
{
snprintf(device, sizeof(device), "/dev/bpf%d", n);
}
fd = open(device, O_RDWR);
if (n++ < 0 && fd < 0 && errno == ENOENT)
{
no_cloning_bpf = 1;
errno = EBUSY;
}
}
while (fd < 0 && errno == EBUSY && n < 1000);
return fd;
}
CALLBACK(handler_onpkt, bool,
pf_socket_t *socket, int fd, watcher_event_t event)
{
struct bpf_hdr *bh;
void *a;
uint8_t *p = socket->bufdat;
ssize_t n;
size_t pktlen;
n = read(socket->fd, socket->bufdat, socket->buflen);
if (n <= 0)
{
DBG1(DBG_NET, "reading %s request from %s failed: %s",
socket->this->name, socket->if_name, strerror(errno));
return FALSE;
}
while (p < socket->bufdat + n)
{
bh = (struct bpf_hdr*) p;
a = (void*)(p + bh->bh_hdrlen + sizeof(struct ether_header));
pktlen = bh->bh_caplen - sizeof(struct ether_header);
socket->this->handler(socket->this->ctx, socket->if_name,
socket->if_index, socket->mac,
socket->fd, chunk_create(a, pktlen));
p += BPF_WORDALIGN(bh->bh_hdrlen + bh->bh_caplen);
}
return TRUE;
}
/**
* Create and initialize a BPF socket for the interface specified in the given
* struct. This entails opening a BPF device, binding it to the interface,
* setting the packet filter, and allocating a buffer for receiving packets.
*/
static bool setup_pf_socket(pf_socket_t *socket, pf_program_t *program)
{
struct ifreq req;
uint32_t disable = 1;
uint32_t enable = 1;
uint32_t dlt = 0;
snprintf(req.ifr_name, sizeof(req.ifr_name), "%s", socket->if_name);
if ((socket->fd = bpf_open()) < 0)
{
DBG1(DBG_NET, "bpf_open(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCSETIF, &req) < 0)
{
DBG1(DBG_NET, "BIOCSETIF(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCSHDRCMPLT, &enable) < 0)
{
DBG1(DBG_NET, "BIOCSHDRCMPLT(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCSSEESENT, &disable) < 0)
{
DBG1(DBG_NET, "BIOCSSEESENT(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCIMMEDIATE, &enable) < 0)
{
DBG1(DBG_NET, "BIOCIMMEDIATE(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCGDLT, &dlt) < 0)
{
DBG1(DBG_NET, "BIOCGDLT(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
else if (dlt != DLT_EN10MB)
{
return FALSE;
}
if (ioctl(socket->fd, BIOCSETF, program) < 0)
{
DBG1(DBG_NET, "BIOCSETF(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
if (ioctl(socket->fd, BIOCGBLEN, &socket->buflen) < 0)
{
DBG1(DBG_NET, "BIOCGBLEN(%s): %s", socket->if_name, strerror(errno));
return FALSE;
}
socket->bufdat = malloc(socket->buflen);
lib->watcher->add(lib->watcher, socket->fd, WATCHER_READ,
handler_onpkt, socket);
return TRUE;
}
/**
* Create a socket for each BPF capable interface. The interface must have an
* Ethernet MAC address, an IPv4 address, and use an Ethernet data link layer.
*/
static bool setup_internal(private_pf_handler_t *this, char *iface,
pf_program_t *program)
{
struct ifaddrs *ifas;
struct ifaddrs *ifa;
struct sockaddr_dl *dl;
pf_socket_t *socket;
enumerator_t *enumerator;
host_t *ipv4;
if (getifaddrs(&ifas) < 0)
{
DBG1(DBG_NET, "%s cannot find interfaces: %s", this->name, strerror(errno));
return FALSE;
}
this->pf_sockets = linked_list_create();
for (ifa = ifas; ifa != NULL; ifa = ifa->ifa_next)
{
switch (ifa->ifa_addr->sa_family)
{
case AF_LINK:
dl = (struct sockaddr_dl *)ifa->ifa_addr;
if (dl->sdl_alen == ETHER_ADDR_LEN)
{
socket = get_pf_socket(this, ifa->ifa_name);
socket->if_index = dl->sdl_index;
socket->mac = chunk_clone(chunk_create(LLADDR(dl),
dl->sdl_alen));
}
break;
case AF_INET:
ipv4 = host_create_from_sockaddr(ifa->ifa_addr);
if (ipv4 && !ipv4->is_anyaddr(ipv4))
{
socket = get_pf_socket(this, ifa->ifa_name);
if (!socket->ipv4)
{
socket->ipv4 = ipv4->clone(ipv4);
}
}
DESTROY_IF(ipv4);
break;
default:
break;
}
}
freeifaddrs(ifas);
enumerator = this->pf_sockets->create_enumerator(this->pf_sockets);
while (enumerator->enumerate(enumerator, &socket))
{
if (socket->mac.ptr && socket->ipv4 &&
(!iface || streq(socket->if_name, iface)) &&
setup_pf_socket(socket, program))
{
DBG2(DBG_NET, "listening for %s requests on %s (%H, %#B)",
this->name, socket->if_name, socket->ipv4, &socket->mac);
}
else
{
this->pf_sockets->remove_at(this->pf_sockets, enumerator);
destroy_pf_socket(socket);
}
}
enumerator->destroy(enumerator);
return this->pf_sockets->get_count(this->pf_sockets) > 0;
}
METHOD(pf_handler_t, destroy, void,
private_pf_handler_t *this)
{
DESTROY_FUNCTION_IF(this->pf_sockets, destroy_pf_socket);
free(this);
}
/*
* Described in header
*/
bool bind_to_device(int fd, char *iface)
{
#if defined(__FreeBSD__)
DBG1(DBG_CFG, "binding socket to '%s' failed: IP_SENDIF not implemented yet.", iface);
return FALSE;
#else /* defined(__FreeBSD__) */
unsigned int idx = if_nametoindex(iface);
if (setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &idx, sizeof(idx)) == -1)
{
DBG1(DBG_CFG, "binding socket to '%s' failed: %s",
iface, strerror(errno));
return FALSE;
}
return TRUE;
#endif /* defined(__FreeBSD__) */
}
#endif /* !defined(__APPLE__) && !defined(__FreeBSD__) */
/*
* Described in header
*/
pf_handler_t *pf_handler_create(const char *name, char *iface,
pf_packet_handler_t handler, void *ctx,
pf_program_t *program)
{
private_pf_handler_t *this;
INIT(this,
.public = {
.destroy = _destroy,
},
.name = name,
.handler = handler,
.ctx = ctx,
);
if (!setup_internal(this, iface, program))
{
destroy(this);
return NULL;
}
return &this->public;
}