/*
 * Virtio Network Device
 *
 * Copyright IBM, Corp. 2007
 *
 * Authors:
 *  Anthony Liguori   <aliguori@us.ibm.com>
 *
 * This work is licensed under the terms of the GNU GPL, version 2.  See
 * the COPYING file in the top-level directory.
 *
 */

#include "virtio.h"
#include "net.h"
#include "qemu-timer.h"
#include "qemu-kvm.h"

/* from Linux's virtio_net.h */

/* The ID for virtio_net */
#define VIRTIO_ID_NET	1

/* The feature bitmap for virtio net */
#define VIRTIO_NET_F_CSUM	0	/* Host handles pkts w/ partial csum */
#define VIRTIO_NET_F_GUEST_CSUM	1	/* Guest handles pkts w/ partial csum */
#define VIRTIO_NET_F_MAC	5	/* Host has given MAC address. */
#define VIRTIO_NET_F_GSO	6	/* Host handles pkts w/ any GSO type */
#define VIRTIO_NET_F_GUEST_TSO4	7	/* Guest can handle TSOv4 in. */
#define VIRTIO_NET_F_GUEST_TSO6	8	/* Guest can handle TSOv6 in. */
#define VIRTIO_NET_F_GUEST_ECN	9	/* Guest can handle TSO[6] w/ ECN in. */
#define VIRTIO_NET_F_GUEST_UFO	10	/* Guest can handle UFO in. */
#define VIRTIO_NET_F_HOST_TSO4	11	/* Host can handle TSOv4 in. */
#define VIRTIO_NET_F_HOST_TSO6	12	/* Host can handle TSOv6 in. */
#define VIRTIO_NET_F_HOST_ECN	13	/* Host can handle TSO[6] w/ ECN in. */
#define VIRTIO_NET_F_HOST_UFO	14	/* Host can handle UFO in. */

#define TX_TIMER_INTERVAL 150000 /* 150 us */

/* The config defining mac address (6 bytes) */
struct virtio_net_config
{
    uint8_t mac[6];
} __attribute__((packed));

/* This is the first element of the scatter-gather list.  If you don't
 * specify GSO or CSUM features, you can simply ignore the header. */
struct virtio_net_hdr
{
#define VIRTIO_NET_HDR_F_NEEDS_CSUM	1	// Use csum_start, csum_offset
    uint8_t flags;
#define VIRTIO_NET_HDR_GSO_NONE		0	// Not a GSO frame
#define VIRTIO_NET_HDR_GSO_TCPV4	1	// GSO frame, IPv4 TCP (TSO)
#define VIRTIO_NET_HDR_GSO_UDP		3	// GSO frame, IPv4 UDP (UFO)
#define VIRTIO_NET_HDR_GSO_TCPV6	4	// GSO frame, IPv6 TCP
#define VIRTIO_NET_HDR_GSO_ECN		0x80	// TCP has ECN set
    uint8_t gso_type;
    uint16_t hdr_len;
    uint16_t gso_size;
    uint16_t csum_start;
    uint16_t csum_offset;
};

typedef struct VirtIONet
{
    VirtIODevice vdev;
    uint8_t mac[6];
    VirtQueue *rx_vq;
    VirtQueue *tx_vq;
    VLANClientState *vc;
    QEMUTimer *tx_timer;
    int tx_timer_active;
} VirtIONet;

/* TODO
 * - we could suppress RX interrupt if we were so inclined.
 */

static VirtIONet *to_virtio_net(VirtIODevice *vdev)
{
    return (VirtIONet *)vdev;
}

static void virtio_net_update_config(VirtIODevice *vdev, uint8_t *config)
{
    VirtIONet *n = to_virtio_net(vdev);
    struct virtio_net_config netcfg;

    memcpy(netcfg.mac, n->mac, 6);
    memcpy(config, &netcfg, sizeof(netcfg));
}

static uint32_t virtio_net_get_features(VirtIODevice *vdev)
{
    VirtIONet *n = to_virtio_net(vdev);
    VLANClientState *host = n->vc->vlan->first_client;
    uint32_t features = (1 << VIRTIO_NET_F_MAC);

    if (tap_has_vnet_hdr(host)) {
	tap_using_vnet_hdr(host, 1);
	features |= (1 << VIRTIO_NET_F_CSUM);
	features |= (1 << VIRTIO_NET_F_GUEST_CSUM);
	features |= (1 << VIRTIO_NET_F_GUEST_TSO4);
	features |= (1 << VIRTIO_NET_F_GUEST_TSO6);
	features |= (1 << VIRTIO_NET_F_GUEST_ECN);
	features |= (1 << VIRTIO_NET_F_HOST_TSO4);
	features |= (1 << VIRTIO_NET_F_HOST_TSO6);
	features |= (1 << VIRTIO_NET_F_HOST_ECN);
	/* Kernel can't actually handle UFO in software currently. */
    }

    return features;
}

static void virtio_net_set_features(VirtIODevice *vdev, uint32_t features)
{
    VirtIONet *n = to_virtio_net(vdev);
    VLANClientState *host = n->vc->vlan->first_client;

    if (!tap_has_vnet_hdr(host) || !host->set_offload)
	return;

    host->set_offload(host,
		      (features >> VIRTIO_NET_F_GUEST_CSUM) & 1,
		      (features >> VIRTIO_NET_F_GUEST_TSO4) & 1,
		      (features >> VIRTIO_NET_F_GUEST_TSO6) & 1,
		      (features >> VIRTIO_NET_F_GUEST_ECN)  & 1);
}

/* RX */

static void virtio_net_handle_rx(VirtIODevice *vdev, VirtQueue *vq)
{
    /* We now have RX buffers, signal to the IO thread to break out of the
       select to re-poll the tap file descriptor */
    if (kvm_enabled())
	qemu_kvm_notify_work();
}

static int virtio_net_can_receive(void *opaque)
{
    VirtIONet *n = opaque;

    if (n->rx_vq->vring.avail == NULL ||
	!(n->vdev.status & VIRTIO_CONFIG_S_DRIVER_OK))
	return 0;

    if (n->rx_vq->vring.avail->idx == n->rx_vq->last_avail_idx) {
	n->rx_vq->vring.used->flags &= ~VRING_USED_F_NO_NOTIFY;
	return 0;
    }

    n->rx_vq->vring.used->flags |= VRING_USED_F_NO_NOTIFY;
    return 1;
}

/* dhclient uses AF_PACKET but doesn't pass auxdata to the kernel so
 * it never finds out that the packets don't have valid checksums.  This
 * causes dhclient to get upset.  Fedora's carried a patch for ages to
 * fix this with Xen but it hasn't appeared in an upstream release of
 * dhclient yet.
 *
 * To avoid breaking existing guests, we catch udp packets and add
 * checksums.  This is terrible but it's better than hacking the guest
 * kernels.
 *
 * N.B. if we introduce a zero-copy API, this operation is no longer free so
 * we should provide a mechanism to disable it to avoid polluting the host
 * cache.
 */
static void work_around_broken_dhclient(struct virtio_net_hdr *hdr,
                                        const uint8_t *buf, size_t size)
{
    if ((hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) && /* missing csum */
        (size > 27 && size < 1500) && /* normal sized MTU */
        (buf[12] == 0x08 && buf[13] == 0x00) && /* ethertype == IPv4 */
        (buf[23] == 17) && /* ip.protocol == UDP */
        (buf[34] == 0 && buf[35] == 67)) { /* udp.srcport == bootps */
        /* FIXME this cast is evil */
        net_checksum_calculate((uint8_t *)buf, size);
        hdr->flags &= ~VIRTIO_NET_HDR_F_NEEDS_CSUM;
    }
}

static void virtio_net_receive(void *opaque, const uint8_t *buf, int size)
{
    VirtIONet *n = opaque;
    VirtQueueElement elem;
    struct virtio_net_hdr *hdr;
    int offset, i;
    int total;

    if (virtqueue_pop(n->rx_vq, &elem) == 0)
	return;

    if (elem.in_num < 1 || elem.in_sg[0].iov_len != sizeof(*hdr)) {
	fprintf(stderr, "virtio-net header not in first element\n");
	exit(1);
    }

    hdr = (void *)elem.in_sg[0].iov_base;
    hdr->flags = 0;
    hdr->gso_type = VIRTIO_NET_HDR_GSO_NONE;

    offset = 0;
    total = sizeof(*hdr);

    if (tap_has_vnet_hdr(n->vc->vlan->first_client)) {
	memcpy(hdr, buf, sizeof(*hdr));
	offset += total;
        work_around_broken_dhclient(hdr, buf + offset, size - offset);
    }

    /* copy in packet.  ugh */
    i = 1;
    while (offset < size && i < elem.in_num) {
	int len = MIN(elem.in_sg[i].iov_len, size - offset);
	memcpy(elem.in_sg[i].iov_base, buf + offset, len);
	offset += len;
	total += len;
	i++;
    }

    /* signal other side */
    virtqueue_push(n->rx_vq, &elem, total);
    virtio_notify(&n->vdev, n->rx_vq);
}

/* TX */
static void virtio_net_flush_tx(VirtIONet *n, VirtQueue *vq)
{
    VirtQueueElement elem;
    int has_vnet_hdr = tap_has_vnet_hdr(n->vc->vlan->first_client);

    if (!(n->vdev.status & VIRTIO_CONFIG_S_DRIVER_OK))
        return;

    while (virtqueue_pop(vq, &elem)) {
	ssize_t len = 0;
	unsigned int out_num = elem.out_num;
	struct iovec *out_sg = &elem.out_sg[0];

	if (out_num < 1 || out_sg->iov_len != sizeof(struct virtio_net_hdr)) {
	    fprintf(stderr, "virtio-net header not in first element\n");
	    exit(1);
	}

	/* ignore the header if GSO is not supported */
	if (!has_vnet_hdr) {
	    out_num--;
	    out_sg++;
	    len += sizeof(struct virtio_net_hdr);
	}

	len += qemu_sendv_packet(n->vc, out_sg, out_num);

	virtqueue_push(vq, &elem, len);
	virtio_notify(&n->vdev, vq);
    }
}

static void virtio_net_handle_tx(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIONet *n = to_virtio_net(vdev);

    if (n->tx_timer_active) {
	vq->vring.used->flags &= ~VRING_USED_F_NO_NOTIFY;
	qemu_del_timer(n->tx_timer);
	n->tx_timer_active = 0;
	virtio_net_flush_tx(n, vq);
    } else {
	qemu_mod_timer(n->tx_timer,
		       qemu_get_clock(vm_clock) + TX_TIMER_INTERVAL);
	n->tx_timer_active = 1;
	vq->vring.used->flags |= VRING_USED_F_NO_NOTIFY;
    }
}

static void virtio_net_tx_timer(void *opaque)
{
    VirtIONet *n = opaque;

    n->tx_timer_active = 0;

    /* Just in case the driver is not ready on more */
    if (!(n->vdev.status & VIRTIO_CONFIG_S_DRIVER_OK))
        return;

    n->tx_vq->vring.used->flags &= ~VRING_USED_F_NO_NOTIFY;
    virtio_net_flush_tx(n, n->tx_vq);
}

static void virtio_net_save(QEMUFile *f, void *opaque)
{
    VirtIONet *n = opaque;

    virtio_save(&n->vdev, f);

    qemu_put_buffer(f, n->mac, 6);
    qemu_put_be32(f, n->tx_timer_active);
}

static int virtio_net_load(QEMUFile *f, void *opaque, int version_id)
{
    VirtIONet *n = opaque;

    if (version_id != 1)
	return -EINVAL;

    virtio_load(&n->vdev, f);

    qemu_get_buffer(f, n->mac, 6);
    n->tx_timer_active = qemu_get_be32(f);

    if (n->tx_timer_active) {
	qemu_mod_timer(n->tx_timer,
		       qemu_get_clock(vm_clock) + TX_TIMER_INTERVAL);
    }

    return 0;
}

PCIDevice *virtio_net_init(PCIBus *bus, NICInfo *nd, int devfn)
{
    VirtIONet *n;
    static int virtio_net_id;

    n = (VirtIONet *)virtio_init_pci(bus, "virtio-net", 6900, 0x1000,
				     0, VIRTIO_ID_NET,
				     0x02, 0x00, 0x00,
				     6, sizeof(VirtIONet));
    if (!n)
	return NULL;

    n->vdev.get_config = virtio_net_update_config;
    n->vdev.get_features = virtio_net_get_features;
    n->vdev.set_features = virtio_net_set_features;
    n->rx_vq = virtio_add_queue(&n->vdev, 256, virtio_net_handle_rx);
    n->tx_vq = virtio_add_queue(&n->vdev, 256, virtio_net_handle_tx);
    memcpy(n->mac, nd->macaddr, 6);
    n->vc = qemu_new_vlan_client(nd->vlan, virtio_net_receive,
                                 virtio_net_can_receive, n);

    n->tx_timer = qemu_new_timer(vm_clock, virtio_net_tx_timer, n);
    n->tx_timer_active = 0;

    register_savevm("virtio-net", virtio_net_id++, 1,
		    virtio_net_save, virtio_net_load, n);

    return (PCIDevice *)n;
}
