hostapd/src/l2_packet/l2_packet_ndis.c
Jouni Malinen e6dd8196e5 Work around Linux packet socket regression
Linux kernel commit 576eb62598f10c8c7fd75703fe89010cdcfff596 ('bridge:
respect RFC2863 operational state') from 2012 introduced a regression
for using wpa_supplicant with EAPOL frames and a station interface in a
bridge. Since it does not look like this regression is going to get
fixed any time soon (it is already two years from that commit and over
1.5 from a discussion pointing out the regression), add a workaround in
wpa_supplicant to avoid this issue.

The wpa_supplicant workaround uses a secondary packet socket to capture
all frames (ETH_P_ALL) from the netdev that is in a bridge. This is
needed to avoid the kernel regression. However, this comes at the price
of more CPU load. Some of this is avoided with use of Linux socket
filter, but still, this is less efficient than a packet socket bound to
the specific EAPOL ethertype. The workaround gets disabled
automatically, if the main packet socket interface on the bridge
interface turns out to be working for RX (e.g., due to an old kernel
version being used or a new kernel version having a fix for the
regression). In addition, this workaround is only taken into use for the
special case of running wpa_supplicant with an interface in a bridge.

Signed-off-by: Jouni Malinen <j@w1.fi>
2015-01-31 17:21:58 +02:00

535 lines
14 KiB
C

/*
* WPA Supplicant - Layer2 packet handling with Microsoft NDISUIO
* Copyright (c) 2003-2006, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*
* This implementation requires Windows specific event loop implementation,
* i.e., eloop_win.c. In addition, the NDISUIO connection is shared with
* driver_ndis.c, so only that driver interface can be used and
* CONFIG_USE_NDISUIO must be defined.
*
* WinXP version of the code uses overlapped I/O and a single threaded design
* with callback functions from I/O code. WinCE version uses a separate RX
* thread that blocks on ReadFile() whenever the media status is connected.
*/
#include "includes.h"
#include <winsock2.h>
#include <ntddndis.h>
#ifdef _WIN32_WCE
#include <winioctl.h>
#include <nuiouser.h>
#endif /* _WIN32_WCE */
#include "common.h"
#include "eloop.h"
#include "l2_packet.h"
#ifndef _WIN32_WCE
/* from nuiouser.h */
#define FSCTL_NDISUIO_BASE FILE_DEVICE_NETWORK
#define _NDISUIO_CTL_CODE(_Function, _Method, _Access) \
CTL_CODE(FSCTL_NDISUIO_BASE, _Function, _Method, _Access)
#define IOCTL_NDISUIO_SET_ETHER_TYPE \
_NDISUIO_CTL_CODE(0x202, METHOD_BUFFERED, \
FILE_READ_ACCESS | FILE_WRITE_ACCESS)
#endif /* _WIN32_WCE */
/* From driver_ndis.c to shared the handle to NDISUIO */
HANDLE driver_ndis_get_ndisuio_handle(void);
/*
* NDISUIO supports filtering of only one ethertype at the time, so we must
* fake support for two (EAPOL and RSN pre-auth) by switching to pre-auth
* whenever wpa_supplicant is trying to pre-authenticate and then switching
* back to EAPOL when pre-authentication has been completed.
*/
struct l2_packet_data;
struct l2_packet_ndisuio_global {
int refcount;
unsigned short first_proto;
struct l2_packet_data *l2[2];
#ifdef _WIN32_WCE
HANDLE rx_thread;
HANDLE stop_request;
HANDLE ready_for_read;
HANDLE rx_processed;
#endif /* _WIN32_WCE */
};
static struct l2_packet_ndisuio_global *l2_ndisuio_global = NULL;
struct l2_packet_data {
char ifname[100];
u8 own_addr[ETH_ALEN];
void (*rx_callback)(void *ctx, const u8 *src_addr,
const u8 *buf, size_t len);
void *rx_callback_ctx;
int l2_hdr; /* whether to include layer 2 (Ethernet) header in calls to
* rx_callback and l2_packet_send() */
HANDLE rx_avail;
#ifndef _WIN32_WCE
OVERLAPPED rx_overlapped;
#endif /* _WIN32_WCE */
u8 rx_buf[1514];
DWORD rx_written;
};
int l2_packet_get_own_addr(struct l2_packet_data *l2, u8 *addr)
{
os_memcpy(addr, l2->own_addr, ETH_ALEN);
return 0;
}
int l2_packet_send(struct l2_packet_data *l2, const u8 *dst_addr, u16 proto,
const u8 *buf, size_t len)
{
BOOL res;
DWORD written;
struct l2_ethhdr *eth;
#ifndef _WIN32_WCE
OVERLAPPED overlapped;
#endif /* _WIN32_WCE */
OVERLAPPED *o;
if (l2 == NULL)
return -1;
#ifdef _WIN32_WCE
o = NULL;
#else /* _WIN32_WCE */
os_memset(&overlapped, 0, sizeof(overlapped));
o = &overlapped;
#endif /* _WIN32_WCE */
if (l2->l2_hdr) {
res = WriteFile(driver_ndis_get_ndisuio_handle(), buf, len,
&written, o);
} else {
size_t mlen = sizeof(*eth) + len;
eth = os_malloc(mlen);
if (eth == NULL)
return -1;
os_memcpy(eth->h_dest, dst_addr, ETH_ALEN);
os_memcpy(eth->h_source, l2->own_addr, ETH_ALEN);
eth->h_proto = htons(proto);
os_memcpy(eth + 1, buf, len);
res = WriteFile(driver_ndis_get_ndisuio_handle(), eth, mlen,
&written, o);
os_free(eth);
}
if (!res) {
DWORD err = GetLastError();
#ifndef _WIN32_WCE
if (err == ERROR_IO_PENDING) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): Wait for pending "
"write to complete");
res = GetOverlappedResult(
driver_ndis_get_ndisuio_handle(), &overlapped,
&written, TRUE);
if (!res) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): "
"GetOverlappedResult failed: %d",
(int) GetLastError());
return -1;
}
return 0;
}
#endif /* _WIN32_WCE */
wpa_printf(MSG_DEBUG, "L2(NDISUIO): WriteFile failed: %d",
(int) GetLastError());
return -1;
}
return 0;
}
static void l2_packet_callback(struct l2_packet_data *l2);
#ifdef _WIN32_WCE
static void l2_packet_rx_thread_try_read(struct l2_packet_data *l2)
{
HANDLE handles[2];
wpa_printf(MSG_MSGDUMP, "l2_packet_rx_thread: -> ReadFile");
if (!ReadFile(driver_ndis_get_ndisuio_handle(), l2->rx_buf,
sizeof(l2->rx_buf), &l2->rx_written, NULL)) {
DWORD err = GetLastError();
wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: ReadFile failed: "
"%d", (int) err);
/*
* ReadFile on NDISUIO/WinCE returns ERROR_DEVICE_NOT_CONNECTED
* error whenever the connection is not up. Yield the thread to
* avoid triggering a busy loop. Connection event should stop
* us from looping for long, but we need to allow enough CPU
* for the main thread to process the media disconnection.
*/
Sleep(100);
return;
}
wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: Read %d byte packet",
(int) l2->rx_written);
/*
* Notify the main thread about the availability of a frame and wait
* for the frame to be processed.
*/
SetEvent(l2->rx_avail);
handles[0] = l2_ndisuio_global->stop_request;
handles[1] = l2_ndisuio_global->rx_processed;
WaitForMultipleObjects(2, handles, FALSE, INFINITE);
ResetEvent(l2_ndisuio_global->rx_processed);
}
static DWORD WINAPI l2_packet_rx_thread(LPVOID arg)
{
struct l2_packet_data *l2 = arg;
DWORD res;
HANDLE handles[2];
int run = 1;
wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread started");
handles[0] = l2_ndisuio_global->stop_request;
handles[1] = l2_ndisuio_global->ready_for_read;
/*
* Unfortunately, NDISUIO on WinCE does not seem to support waiting
* on the handle. There do not seem to be anything else that we could
* wait for either. If one were to modify NDISUIO to set a named event
* whenever packets are available, this event could be used here to
* avoid having to poll for new packets or we could even move to use a
* single threaded design.
*
* In addition, NDISUIO on WinCE is returning
* ERROR_DEVICE_NOT_CONNECTED whenever ReadFile() is attempted while
* the adapter is not in connected state. For now, we are just using a
* local event to allow ReadFile calls only after having received NDIS
* media connect event. This event could be easily converted to handle
* another event if the protocol driver is replaced with somewhat more
* useful design.
*/
while (l2_ndisuio_global && run) {
res = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
switch (res) {
case WAIT_OBJECT_0:
wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: Received "
"request to stop RX thread");
run = 0;
break;
case WAIT_OBJECT_0 + 1:
l2_packet_rx_thread_try_read(l2);
break;
case WAIT_FAILED:
default:
wpa_printf(MSG_DEBUG, "l2_packet_rx_thread: "
"WaitForMultipleObjects failed: %d",
(int) GetLastError());
run = 0;
break;
}
}
wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread stopped");
return 0;
}
#else /* _WIN32_WCE */
static int l2_ndisuio_start_read(struct l2_packet_data *l2, int recursive)
{
os_memset(&l2->rx_overlapped, 0, sizeof(l2->rx_overlapped));
l2->rx_overlapped.hEvent = l2->rx_avail;
if (!ReadFile(driver_ndis_get_ndisuio_handle(), l2->rx_buf,
sizeof(l2->rx_buf), &l2->rx_written, &l2->rx_overlapped))
{
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): ReadFile failed: "
"%d", (int) err);
return -1;
}
/*
* Once read is completed, l2_packet_rx_event() will be
* called.
*/
} else {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): ReadFile returned data "
"without wait for completion");
if (!recursive)
l2_packet_callback(l2);
}
return 0;
}
#endif /* _WIN32_WCE */
static void l2_packet_callback(struct l2_packet_data *l2)
{
const u8 *rx_buf, *rx_src;
size_t rx_len;
struct l2_ethhdr *ethhdr = (struct l2_ethhdr *) l2->rx_buf;
wpa_printf(MSG_DEBUG, "L2(NDISUIO): Read %d bytes",
(int) l2->rx_written);
if (l2->l2_hdr || l2->rx_written < sizeof(*ethhdr)) {
rx_buf = (u8 *) ethhdr;
rx_len = l2->rx_written;
} else {
rx_buf = (u8 *) (ethhdr + 1);
rx_len = l2->rx_written - sizeof(*ethhdr);
}
rx_src = ethhdr->h_source;
l2->rx_callback(l2->rx_callback_ctx, rx_src, rx_buf, rx_len);
#ifndef _WIN32_WCE
l2_ndisuio_start_read(l2, 1);
#endif /* _WIN32_WCE */
}
static void l2_packet_rx_event(void *eloop_data, void *user_data)
{
struct l2_packet_data *l2 = eloop_data;
if (l2_ndisuio_global)
l2 = l2_ndisuio_global->l2[l2_ndisuio_global->refcount - 1];
ResetEvent(l2->rx_avail);
#ifndef _WIN32_WCE
if (!GetOverlappedResult(driver_ndis_get_ndisuio_handle(),
&l2->rx_overlapped, &l2->rx_written, FALSE)) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): GetOverlappedResult "
"failed: %d", (int) GetLastError());
return;
}
#endif /* _WIN32_WCE */
l2_packet_callback(l2);
#ifdef _WIN32_WCE
SetEvent(l2_ndisuio_global->rx_processed);
#endif /* _WIN32_WCE */
}
static int l2_ndisuio_set_ether_type(unsigned short protocol)
{
USHORT proto = htons(protocol);
DWORD written;
if (!DeviceIoControl(driver_ndis_get_ndisuio_handle(),
IOCTL_NDISUIO_SET_ETHER_TYPE, &proto,
sizeof(proto), NULL, 0, &written, NULL)) {
wpa_printf(MSG_ERROR, "L2(NDISUIO): "
"IOCTL_NDISUIO_SET_ETHER_TYPE failed: %d",
(int) GetLastError());
return -1;
}
return 0;
}
struct l2_packet_data * l2_packet_init(
const char *ifname, const u8 *own_addr, unsigned short protocol,
void (*rx_callback)(void *ctx, const u8 *src_addr,
const u8 *buf, size_t len),
void *rx_callback_ctx, int l2_hdr)
{
struct l2_packet_data *l2;
if (l2_ndisuio_global == NULL) {
l2_ndisuio_global = os_zalloc(sizeof(*l2_ndisuio_global));
if (l2_ndisuio_global == NULL)
return NULL;
l2_ndisuio_global->first_proto = protocol;
}
if (l2_ndisuio_global->refcount >= 2) {
wpa_printf(MSG_ERROR, "L2(NDISUIO): Not more than two "
"simultaneous connections allowed");
return NULL;
}
l2_ndisuio_global->refcount++;
l2 = os_zalloc(sizeof(struct l2_packet_data));
if (l2 == NULL)
return NULL;
l2_ndisuio_global->l2[l2_ndisuio_global->refcount - 1] = l2;
os_strlcpy(l2->ifname, ifname, sizeof(l2->ifname));
l2->rx_callback = rx_callback;
l2->rx_callback_ctx = rx_callback_ctx;
l2->l2_hdr = l2_hdr;
if (own_addr)
os_memcpy(l2->own_addr, own_addr, ETH_ALEN);
if (l2_ndisuio_set_ether_type(protocol) < 0) {
os_free(l2);
return NULL;
}
if (l2_ndisuio_global->refcount > 1) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): Temporarily setting "
"filtering ethertype to %04x", protocol);
if (l2_ndisuio_global->l2[0])
l2->rx_avail = l2_ndisuio_global->l2[0]->rx_avail;
return l2;
}
l2->rx_avail = CreateEvent(NULL, TRUE, FALSE, NULL);
if (l2->rx_avail == NULL) {
os_free(l2);
return NULL;
}
eloop_register_event(l2->rx_avail, sizeof(l2->rx_avail),
l2_packet_rx_event, l2, NULL);
#ifdef _WIN32_WCE
l2_ndisuio_global->stop_request = CreateEvent(NULL, TRUE, FALSE, NULL);
/*
* This event is being set based on media connect/disconnect
* notifications in driver_ndis.c.
*/
l2_ndisuio_global->ready_for_read =
CreateEvent(NULL, TRUE, FALSE, TEXT("WpaSupplicantConnected"));
l2_ndisuio_global->rx_processed = CreateEvent(NULL, TRUE, FALSE, NULL);
if (l2_ndisuio_global->stop_request == NULL ||
l2_ndisuio_global->ready_for_read == NULL ||
l2_ndisuio_global->rx_processed == NULL) {
if (l2_ndisuio_global->stop_request) {
CloseHandle(l2_ndisuio_global->stop_request);
l2_ndisuio_global->stop_request = NULL;
}
if (l2_ndisuio_global->ready_for_read) {
CloseHandle(l2_ndisuio_global->ready_for_read);
l2_ndisuio_global->ready_for_read = NULL;
}
if (l2_ndisuio_global->rx_processed) {
CloseHandle(l2_ndisuio_global->rx_processed);
l2_ndisuio_global->rx_processed = NULL;
}
eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
os_free(l2);
return NULL;
}
l2_ndisuio_global->rx_thread = CreateThread(NULL, 0,
l2_packet_rx_thread, l2, 0,
NULL);
if (l2_ndisuio_global->rx_thread == NULL) {
wpa_printf(MSG_INFO, "L2(NDISUIO): Failed to create RX "
"thread: %d", (int) GetLastError());
eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
CloseHandle(l2_ndisuio_global->stop_request);
l2_ndisuio_global->stop_request = NULL;
os_free(l2);
return NULL;
}
#else /* _WIN32_WCE */
l2_ndisuio_start_read(l2, 0);
#endif /* _WIN32_WCE */
return l2;
}
struct l2_packet_data * l2_packet_init_bridge(
const char *br_ifname, const char *ifname, const u8 *own_addr,
unsigned short protocol,
void (*rx_callback)(void *ctx, const u8 *src_addr,
const u8 *buf, size_t len),
void *rx_callback_ctx, int l2_hdr)
{
return l2_packet_init(br_ifname, own_addr, protocol, rx_callback,
rx_callback_ctx, l2_hdr);
}
void l2_packet_deinit(struct l2_packet_data *l2)
{
if (l2 == NULL)
return;
if (l2_ndisuio_global) {
l2_ndisuio_global->refcount--;
l2_ndisuio_global->l2[l2_ndisuio_global->refcount] = NULL;
if (l2_ndisuio_global->refcount) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): restore filtering "
"ethertype to %04x",
l2_ndisuio_global->first_proto);
l2_ndisuio_set_ether_type(
l2_ndisuio_global->first_proto);
return;
}
#ifdef _WIN32_WCE
wpa_printf(MSG_DEBUG, "L2(NDISUIO): Waiting for RX thread to "
"stop");
SetEvent(l2_ndisuio_global->stop_request);
/*
* Cancel pending ReadFile() in the RX thread (if we were still
* connected at this point).
*/
if (!DeviceIoControl(driver_ndis_get_ndisuio_handle(),
IOCTL_CANCEL_READ, NULL, 0, NULL, 0, NULL,
NULL)) {
wpa_printf(MSG_DEBUG, "L2(NDISUIO): IOCTL_CANCEL_READ "
"failed: %d", (int) GetLastError());
/* RX thread will exit blocking ReadFile once NDISUIO
* notices that the adapter is disconnected. */
}
WaitForSingleObject(l2_ndisuio_global->rx_thread, INFINITE);
wpa_printf(MSG_DEBUG, "L2(NDISUIO): RX thread exited");
CloseHandle(l2_ndisuio_global->rx_thread);
CloseHandle(l2_ndisuio_global->stop_request);
CloseHandle(l2_ndisuio_global->ready_for_read);
CloseHandle(l2_ndisuio_global->rx_processed);
#endif /* _WIN32_WCE */
os_free(l2_ndisuio_global);
l2_ndisuio_global = NULL;
}
#ifndef _WIN32_WCE
CancelIo(driver_ndis_get_ndisuio_handle());
#endif /* _WIN32_WCE */
eloop_unregister_event(l2->rx_avail, sizeof(l2->rx_avail));
CloseHandle(l2->rx_avail);
os_free(l2);
}
int l2_packet_get_ip_addr(struct l2_packet_data *l2, char *buf, size_t len)
{
return -1;
}
void l2_packet_notify_auth_start(struct l2_packet_data *l2)
{
}
int l2_packet_set_packet_filter(struct l2_packet_data *l2,
enum l2_packet_filter_type type)
{
return -1;
}