/*
 *  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.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * Purpose: VDE networking support.
 *
 * Author: Stas Sergeev <stsp@users.sourceforge.net>
 */

#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <libvdeplug.h>
#include "emu.h"
#include "init.h"
#include "utilities.h"
#include "pktdrvr.h"
#include "sig.h"

static VDECONN *vde;
static struct popen2 vdesw, slirp;
static pthread_t open_thr;

static void vde_exit(void *arg)
{
    error("vde failed, exiting\n");
    leavedos(35);
}

static void err_handler(void *arg)
{
    error("VDE startup failure\n");
}

static char *start_vde(void)
{
    char cmd[256];
    int err, n, started, fd, pmin, pmax, q;
    fd_set fds;
    struct timeval tv;
    char *nam = tmpnam(NULL);
    fd = -1;
    q = (pkt_get_flags() & PKT_FLG_QUIET);
    if (q) {
	fd = dup(STDERR_FILENO);
	close(STDERR_FILENO);
	open("/dev/null", O_WRONLY);
    }
    snprintf(cmd, sizeof(cmd), "vde_switch -s %s", nam);
    err = popen2_custom(cmd, &vdesw);
    if (fd != -1) {
	close(STDERR_FILENO);
	dup(fd);
	close(fd);
    }
    if (err) {
	error("failed to start %s\n", cmd);
	goto fail2;
    }
    FD_ZERO(&fds);
    FD_SET(vdesw.from_child, &fds);
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    pthread_cleanup_push(err_handler, NULL);
    err = select(vdesw.from_child + 1, &fds, NULL, NULL, &tv);
    pthread_cleanup_pop(0);
    switch(err) {
    case -1:
	error("select failed: %s\n", strerror(errno));
	goto fail1;
    case 0:
	if (!q)
	    error("you appear to have unpatched vde\n");
	break;
    default:
	n = read(vdesw.from_child, cmd, sizeof(cmd) - 1);
	switch (n) {
	case -1:
	    error("read failed: %s\n", strerror(errno));
	    goto fail1;
	case 0:
	    if (!q)
		error("failed to start %s\n", cmd);
	    goto fail1;
	}
	cmd[n] = 0;
	if (!strstr(cmd, " started")) {
	    error("vde_switch failed: %s\n", cmd);
	    goto fail1;
	}

	/* check for userspace pings */
	fd = open("/proc/sys/net/ipv4/ping_group_range", O_RDONLY);
	if (fd == -1) {
	    pd_printf("PKT: no /proc/sys/net/ipv4/ping_group_range??\n");
	    goto out;
	}
	n = read(fd, cmd, sizeof(cmd) - 1);
	if (n <= 0)
	    goto out2;
	cmd[n] = 0;
	sscanf(cmd, "%i %i", &pmin, &pmax);
	/* this is not a strict check, but default value is 1 0 */
	if (pmin > pmax)
	    error("userspace pings are disabled.\n"
		    "\tTo enable them, do as root:\n"
		    "\techo 0 65535 > /proc/sys/net/ipv4/ping_group_range\n");
	pd_printf("PKT: ping_group_range is %s", cmd);
out2:
	close(fd);
out:
	break;
    }
    snprintf(cmd, sizeof(cmd), "slirpvde -s %s %s 2>&1", nam,
	     config.slirp_args ? : "");
    err = popen2_custom(cmd, &slirp);
    if (err) {
	error("failed to start %s\n", cmd);
	goto fail1;
    }
    started = 0;
    while (1) {
	n = read(slirp.from_child, cmd, sizeof(cmd) - 1);
	if (n <= 0)
	    break;
	cmd[n] = 0;
	pd_printf("slirp: %s", cmd);
	if (strstr(cmd, "vde switch")) {
	    started = 1;
	    break;
	}
    }
    if (!started) {
	error("failed to start slirpvde\n");
	goto fail0;
    }

    pd_printf("PKT: started VDE at %s\n", nam);
    return nam;

  fail0:
    pclose2(&slirp);
  fail1:
    pclose2(&vdesw);
  fail2:
    return NULL;
}

struct cbk_data {
    VDECONN *vde;
    void (*cbk)(int, int);
};

static void pkt_register_cb(void *arg)
{
    struct cbk_data *cbkd = arg;
    vde = cbkd->vde;
    sigchld_register_handler(vdesw.child_pid, vde_exit, NULL);
    sigchld_register_handler(slirp.child_pid, vde_exit, NULL);
    cbkd->cbk(vde_datafd(cbkd->vde), 6);
    free(cbkd);
}

struct thr_data {
    const char *name;
    void (*cbk)(int, int);
};

static void *open_thread(void *arg)
{
    struct thr_data *thrd = arg;
    const char *name = thrd->name;
    char *name2;
    VDECONN *vde;
    struct cbk_data *cbkd;

    if (!name[0]) {
	name = start_vde();
	if (!name)
	    return NULL;
    }
    name2 = strdup("dosemu");
    vde = vde_open(name, name2, NULL);
    free(name2);
    if (!vde)
	return NULL;
    cbkd = malloc(sizeof(*cbkd));
    cbkd->vde = vde;
    cbkd->cbk = thrd->cbk;
    add_thread_callback(pkt_register_cb, cbkd, "vde");
    free(thrd);
    return NULL;
}

static int OpenNetworkLinkVde(const char *name, void (*cbk)(int, int))
{
    int ret;
    struct thr_data *thrd = malloc(sizeof(*thrd));
    thrd->name = name;
    thrd->cbk = cbk;
    /* need to open in a separate thread as waiting for startup
     * may be long if the vde is unpatched */
    ret = pthread_create(&open_thr, NULL, open_thread, thrd);
#if defined(HAVE_PTHREAD_SETNAME_NP) && defined(__GLIBC__)
    pthread_setname_np(open_thr, "dosemu: vde");
#endif
    return ret;
}

static void CloseNetworkLinkVde(int pkt_fd)
{
    pthread_cancel(open_thr);
    pthread_join(open_thr, NULL);
    remove_from_io_select(pkt_fd);
    vde_close(vde);
    sigchld_enable_handler(slirp.child_pid, 0);
    pclose2(&slirp);
    sigchld_enable_handler(vdesw.child_pid, 0);
    pclose2(&vdesw);
}

static int GetDeviceMTUVde(void)
{
    return 1500;
}

static int GetDeviceHardwareAddressVde(unsigned char *addr)
{
    pkt_get_fake_mac(addr);
    return 0;
}

static ssize_t pkt_read_vde(int pkt_fd, void *buf, size_t count)
{
    ssize_t len;
    fd_set set;
    struct timeval tv = { };
    int selrt;
    FD_ZERO(&set);
    FD_SET(pkt_fd, &set);
    /* vde_recv() ignores flags... so we use select() */
    selrt = select(pkt_fd + 1, &set, NULL, NULL, &tv);
    if (selrt <= 0)
	return 0;
    len = vde_recv(vde, buf, count, MSG_DONTWAIT);
    if (len == 0) {
	error("VDE unexpectedly terminated\n");
	leavedos(3);
    }
    if (len < 0)
	error("recv() returned %zi, %s\n", len, strerror(errno));
    return len;
}

static ssize_t pkt_write_vde(int pkt_fd, const void *buf, size_t count)
{
    ssize_t ret;
    ret = vde_send(vde, buf, count, 0);
    if (ret == -1) {
	int err = errno;
	error("vde_send(): %s\n", strerror(err));
	if (err == ENOTCONN || err == ECONNREFUSED) {
	    error("VDE unexpectedly terminated\n");
	    leavedos(3);
	}
    }
    return ret;
}

static struct pkt_ops vde_ops = {
    .id = VNET_TYPE_VDE,
    .open = OpenNetworkLinkVde,
    .close = CloseNetworkLinkVde,
    .get_hw_addr = GetDeviceHardwareAddressVde,
    .get_MTU = GetDeviceMTUVde,
    .pkt_read = pkt_read_vde,
    .pkt_write = pkt_write_vde,
    .flags = PFLG_ASYNC,
};

CONSTRUCTOR(static void vde_init(void))
{
    pkt_register_backend(&vde_ops);
}
