root/src/add-ons/kernel/network/devices/ethernet/ethernet.cpp
/*
 * Copyright 2006-2009, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Axel Dörfler, axeld@pinc-software.de
 */


#include <ethernet.h>

#include <ether_driver.h>
#include <net_buffer.h>
#include <net_device.h>
#include <net_stack.h>

#include <lock.h>
#include <util/AutoLock.h>
#include <util/DoublyLinkedList.h>

#include <KernelExport.h>

#include <errno.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/if_media.h>
#include <net/if_types.h>
#include <new>
#include <stdlib.h>
#include <string.h>


struct ethernet_device : net_device, DoublyLinkedListLinkImpl<ethernet_device> {
        int             fd;
        uint32  frame_size;
        bool    supports_net_buffer;
};

static const bigtime_t kLinkCheckInterval = 1000000;
        // 1 second

net_buffer_module_info *gBufferModule;
static net_stack_module_info *sStackModule;

static mutex sListLock;
static DoublyLinkedList<ethernet_device> sCheckList;
static sem_id sLinkChangeSemaphore;
static thread_id sLinkCheckerThread;


static status_t
update_link_state(ethernet_device *device, bool notify = true)
{
        ether_link_state state;
        if (ioctl(device->fd, ETHER_GET_LINK_STATE, &state,
                        sizeof(ether_link_state)) < 0) {
                // This device does not support retrieving the link
                return B_NOT_SUPPORTED;
        }

        if (device->media != state.media
                || device->link_quality != state.quality
                || device->link_speed != state.speed) {
                device->media = state.media;
                device->link_quality = state.quality;
                device->link_speed = state.speed;

                if (device->media & IFM_ACTIVE) {
                        if ((device->flags & IFF_LINK) == 0) {
                                dprintf("%s: link up, media 0x%0x quality %u speed %u\n",
                                        device->name, (unsigned int)device->media,
                                        (unsigned int)device->link_quality,
                                        (unsigned int)device->link_speed);
                        }
                        device->flags |= IFF_LINK;
                } else {
                        if ((device->flags & IFF_LINK) != 0) {
                                dprintf("%s: link down, media 0x%0x quality %u speed %u\n",
                                        device->name, (unsigned int)device->media,
                                        (unsigned int)device->link_quality,
                                        (unsigned int)device->link_speed);
                        }
                        device->flags &= ~IFF_LINK;
                }


                if (notify)
                        sStackModule->device_link_changed(device);
        }

        return B_OK;
}


static status_t
ethernet_link_checker(void *)
{
        while (true) {
                status_t status = acquire_sem_etc(sLinkChangeSemaphore, 1,
                        B_RELATIVE_TIMEOUT, kLinkCheckInterval);
                if (status == B_BAD_SEM_ID)
                        break;

                MutexLocker _(sListLock);

                if (sCheckList.IsEmpty())
                        break;

                // check link state of all existing devices

                DoublyLinkedList<ethernet_device>::Iterator iterator
                        = sCheckList.GetIterator();
                while (iterator.HasNext()) {
                        update_link_state(iterator.Next());
                }
        }

        return B_OK;
}


//      #pragma mark -


status_t
ethernet_init(const char *name, net_device **_device)
{
        // Make sure this is a device in /dev/net, but not the
        // networking (userland) stack driver.
        // Also make sure the user didn't pass a path like
        // /dev/net/../etc.
        if (strncmp(name, "/dev/net/", 9)
                || !strcmp(name, "/dev/net/userland_server")
                || strstr(name, "..") != NULL)
                return B_BAD_VALUE;

        if (access(name, F_OK) != 0)
                return errno;

        status_t status = get_module(NET_BUFFER_MODULE_NAME, (module_info **)&gBufferModule);
        if (status < B_OK)
                return status;

        ethernet_device *device = new (std::nothrow) ethernet_device;
        if (device == NULL) {
                put_module(NET_BUFFER_MODULE_NAME);
                return B_NO_MEMORY;
        }

        memset(device, 0, sizeof(ethernet_device));

        strcpy(device->name, name);
        device->flags = IFF_BROADCAST | IFF_LINK;
        device->type = IFT_ETHER;
        device->mtu = ETHER_MAX_FRAME_SIZE - ETHER_HEADER_LENGTH;
        device->media = IFM_ACTIVE | IFM_ETHER;
        device->header_length = ETHER_HEADER_LENGTH;
        device->fd = -1;

        *_device = device;
        return B_OK;
}


status_t
ethernet_uninit(net_device *device)
{
        put_module(NET_BUFFER_MODULE_NAME);
        delete device;

        return B_OK;
}


status_t
ethernet_up(net_device *_device)
{
        ethernet_device *device = (ethernet_device *)_device;

        device->fd = open(device->name, O_RDWR);
        if (device->fd < 0)
                return errno;

        uint64 dummy;
        if (ioctl(device->fd, ETHER_INIT, &dummy, sizeof(dummy)) < 0)
                goto err;

        if (ioctl(device->fd, ETHER_GETADDR, device->address.data, ETHER_ADDRESS_LENGTH) < 0)
                goto err;

        if (ioctl(device->fd, ETHER_SEND_NET_BUFFER, NULL, 0) != 0) {
                // Check if the returned error code is B_BAD_DATA (not EINVAL or EOPNOTSUPP).
                if (errno == B_BAD_DATA)
                        device->supports_net_buffer = true;
        }

        if (ioctl(device->fd, ETHER_GETFRAMESIZE, &device->frame_size, sizeof(uint32)) < 0) {
                // this call is obviously optional
                device->frame_size = ETHER_MAX_FRAME_SIZE;
        }

#if 1
        // The network stack does not handle path MTU discovery correctly at present,
        // so don't report frame sizes larger than the standard ethernet maximum.
        // (We will still handle receiving frames larger than this.)
        if (device->frame_size > ETHER_MAX_FRAME_SIZE)
                device->frame_size = ETHER_MAX_FRAME_SIZE;
#endif

        if (update_link_state(device, false) == B_OK) {
                // device supports retrieval of the link state

                // Set the change notification semaphore; doesn't matter
                // if this is supported by the device or not
                ioctl(device->fd, ETHER_SET_LINK_STATE_SEM, &sLinkChangeSemaphore,
                        sizeof(sem_id));

                MutexLocker _(&sListLock);

                if (sCheckList.IsEmpty()) {
                        // start thread
                        sLinkCheckerThread = spawn_kernel_thread(ethernet_link_checker,
                                "ethernet link state checker", B_LOW_PRIORITY, NULL);
                        if (sLinkCheckerThread >= B_OK)
                                resume_thread(sLinkCheckerThread);
                }

                sCheckList.Add(device);
        }

        device->address.length = ETHER_ADDRESS_LENGTH;
        device->mtu = device->frame_size - device->header_length;
        return B_OK;

err:
        close(device->fd);
        device->fd = -1;
        return errno;
}


void
ethernet_down(net_device *_device)
{
        ethernet_device *device = (ethernet_device *)_device;

        MutexLocker _(sListLock);

        // if the device is still part of the list, remove it
        if (device->GetDoublyLinkedListLink()->next != NULL
                || device->GetDoublyLinkedListLink()->previous != NULL
                || device == sCheckList.Head())
                sCheckList.Remove(device);

        close(device->fd);
        device->fd = -1;
}


status_t
ethernet_control(net_device *_device, int32 op, void *argument,
        size_t length)
{
        ethernet_device *device = (ethernet_device *)_device;
        if (ioctl(device->fd, op, argument, length) < 0)
                return errno;
        return B_OK;
}


status_t
ethernet_send_data(net_device *_device, net_buffer *buffer)
{
        ethernet_device *device = (ethernet_device *)_device;

//dprintf("try to send ethernet packet of %lu bytes (flags %ld):\n", buffer->size, buffer->flags);
        if (buffer->size > device->frame_size || buffer->size < ETHER_HEADER_LENGTH)
                return B_BAD_VALUE;

        if (device->supports_net_buffer) {
                if (ioctl(device->fd, ETHER_SEND_NET_BUFFER, buffer, sizeof(net_buffer)) != 0)
                        return errno;
                return 0;
        }

        net_buffer *allocated = NULL;
        net_buffer *original = buffer;

        if (gBufferModule->count_iovecs(buffer) > 1) {
                // Create a new buffer containing the data.
                buffer = gBufferModule->duplicate(original);
                if (buffer == NULL)
                        return ENOBUFS;

                allocated = buffer;

                if (gBufferModule->count_iovecs(buffer) > 1) {
                        dprintf("ethernet: net_buffer I/O is not supported by underlying device\n");
                        gBufferModule->free(buffer);
                        device->stats.send.errors++;
                        return B_NOT_SUPPORTED;
                }
        }

        struct iovec iovec;
        gBufferModule->get_iovecs(buffer, &iovec, 1);

//dump_block((const char *)iovec.iov_base, buffer->size, "  ");
        ssize_t bytesWritten = write(device->fd, iovec.iov_base, iovec.iov_len);
//dprintf("sent: %ld\n", bytesWritten);

        if (bytesWritten < 0) {
                if (allocated)
                        gBufferModule->free(allocated);
                return errno;
        }

        gBufferModule->free(original);
        if (allocated)
                gBufferModule->free(allocated);
        return B_OK;
}


status_t
ethernet_receive_data(net_device *_device, net_buffer **_buffer)
{
        ethernet_device *device = (ethernet_device *)_device;

        if (device->fd == -1)
                return B_FILE_ERROR;

        if (device->supports_net_buffer) {
                if (ioctl(device->fd, ETHER_RECEIVE_NET_BUFFER, _buffer, sizeof(net_buffer*)) != 0)
                        return errno;
                return 0;
        }

        // read/write only works for standard ethernet frames. For larger frames,
        // the driver should support send/receive of net_buffers directly, above.

        net_buffer *buffer = gBufferModule->create(256);
        if (buffer == NULL)
                return ENOBUFS;

        ssize_t bytesRead;
        void *data;

        status_t status = gBufferModule->append_size(buffer, device->frame_size, &data);
        if (status == B_OK && data == NULL) {
                dprintf("ethernet: net_buffer I/O is not supported by underlying device\n");
                status = B_NOT_SUPPORTED;
        }
        if (status < B_OK)
                goto err;

        bytesRead = read(device->fd, data, device->frame_size);
        if (bytesRead < 0) {
                status = errno;
                goto err;
        }
//dump_block((const char *)data, bytesRead, "rcv: ");

        status = gBufferModule->trim(buffer, bytesRead);
        if (status < B_OK) {
                atomic_add((int32*)&device->stats.receive.dropped, 1);
                goto err;
        }

        *_buffer = buffer;
        return B_OK;

err:
        gBufferModule->free(buffer);
        return status;
}


status_t
ethernet_set_mtu(net_device *_device, size_t mtu)
{
        ethernet_device *device = (ethernet_device *)_device;

        if (mtu > device->frame_size - ETHER_HEADER_LENGTH
                || mtu <= ETHER_HEADER_LENGTH + 10)
                return B_BAD_VALUE;

        device->mtu = mtu;
        return B_OK;
}


status_t
ethernet_set_promiscuous(net_device *_device, bool promiscuous)
{
        ethernet_device *device = (ethernet_device *)_device;

        int32 value = (int32)promiscuous;
        if (ioctl(device->fd, ETHER_SETPROMISC, &value, sizeof(value)) < 0)
                return B_NOT_SUPPORTED;

        return B_OK;
}


status_t
ethernet_set_media(net_device *device, uint32 media)
{
        return B_NOT_SUPPORTED;
}


status_t
ethernet_add_multicast(struct net_device *_device, const sockaddr *_address)
{
        ethernet_device *device = (ethernet_device *)_device;

        if (_address->sa_family != AF_LINK)
                return B_BAD_VALUE;

        const sockaddr_dl *address = (const sockaddr_dl *)_address;
        if (address->sdl_type != IFT_ETHER)
                return B_BAD_VALUE;

        if (ioctl(device->fd, ETHER_ADDMULTI, LLADDR(address), 6) < 0)
                return errno;
        return B_OK;
}


status_t
ethernet_remove_multicast(struct net_device *_device, const sockaddr *_address)
{
        ethernet_device *device = (ethernet_device *)_device;

        if (_address->sa_family != AF_LINK)
                return B_BAD_VALUE;

        const sockaddr_dl *address = (const sockaddr_dl *)_address;
        if (address->sdl_type != IFT_ETHER)
                return B_BAD_VALUE;

        if (ioctl(device->fd, ETHER_REMMULTI, LLADDR(address), 6) < 0)
                return errno;
        return B_OK;
}


static status_t
ethernet_std_ops(int32 op, ...)
{
        switch (op) {
                case B_MODULE_INIT:
                {
                        status_t status = get_module(NET_STACK_MODULE_NAME,
                                (module_info **)&sStackModule);
                        if (status < B_OK)
                                return status;

                        new (&sCheckList) DoublyLinkedList<ethernet_device>;
                                // static C++ objects are not initialized in the module startup

                        sLinkCheckerThread = -1;

                        sLinkChangeSemaphore = create_sem(0, "ethernet link change");
                        if (sLinkChangeSemaphore < B_OK) {
                                put_module(NET_STACK_MODULE_NAME);
                                return sLinkChangeSemaphore;
                        }

                        mutex_init(&sListLock, "ethernet devices");

                        return B_OK;
                }

                case B_MODULE_UNINIT:
                {
                        delete_sem(sLinkChangeSemaphore);

                        status_t status;
                        wait_for_thread(sLinkCheckerThread, &status);

                        mutex_destroy(&sListLock);
                        put_module(NET_STACK_MODULE_NAME);
                        return B_OK;
                }

                default:
                        return B_ERROR;
        }
}


net_device_module_info sEthernetModule = {
        {
                "network/devices/ethernet/v1",
                0,
                ethernet_std_ops
        },
        ethernet_init,
        ethernet_uninit,
        ethernet_up,
        ethernet_down,
        ethernet_control,
        ethernet_send_data,
        ethernet_receive_data,
        ethernet_set_mtu,
        ethernet_set_promiscuous,
        ethernet_set_media,
        ethernet_add_multicast,
        ethernet_remove_multicast,
};

module_info *modules[] = {
        (module_info *)&sEthernetModule,
        NULL
};