/*
 * passt network backend
 *
 * Copyright Red Hat
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "qemu/osdep.h"
#include <glib/gstdio.h>
#include "qemu/error-report.h"
#include <gio/gio.h>
#include "net/net.h"
#include "clients.h"
#include "qapi/error.h"
#include "io/net-listener.h"
#include "chardev/char-fe.h"
#include "net/vhost_net.h"
#include "hw/virtio/vhost.h"
#include "hw/virtio/vhost-user.h"
#include "standard-headers/linux/virtio_net.h"
#include "stream_data.h"

#ifdef CONFIG_VHOST_USER
static const int user_feature_bits[] = {
    VIRTIO_F_NOTIFY_ON_EMPTY,
    VIRTIO_F_NOTIFICATION_DATA,
    VIRTIO_RING_F_INDIRECT_DESC,
    VIRTIO_RING_F_EVENT_IDX,

    VIRTIO_F_ANY_LAYOUT,
    VIRTIO_F_VERSION_1,
    VIRTIO_NET_F_CSUM,
    VIRTIO_NET_F_GUEST_CSUM,
    VIRTIO_NET_F_GSO,
    VIRTIO_NET_F_GUEST_TSO4,
    VIRTIO_NET_F_GUEST_TSO6,
    VIRTIO_NET_F_GUEST_ECN,
    VIRTIO_NET_F_GUEST_UFO,
    VIRTIO_NET_F_HOST_TSO4,
    VIRTIO_NET_F_HOST_TSO6,
    VIRTIO_NET_F_HOST_ECN,
    VIRTIO_NET_F_HOST_UFO,
    VIRTIO_NET_F_MRG_RXBUF,
    VIRTIO_NET_F_MTU,
    VIRTIO_F_IOMMU_PLATFORM,
    VIRTIO_F_RING_PACKED,
    VIRTIO_F_RING_RESET,
    VIRTIO_F_IN_ORDER,
    VIRTIO_NET_F_RSS,
    VIRTIO_NET_F_RSC_EXT,
    VIRTIO_NET_F_HASH_REPORT,
    VIRTIO_NET_F_GUEST_USO4,
    VIRTIO_NET_F_GUEST_USO6,
    VIRTIO_NET_F_HOST_USO,

    /* This bit implies RARP isn't sent by QEMU out of band */
    VIRTIO_NET_F_GUEST_ANNOUNCE,

    VIRTIO_NET_F_MQ,

    VHOST_INVALID_FEATURE_BIT
};
#endif

typedef struct NetPasstState {
    NetStreamData data;
    GPtrArray *args;
    gchar *pidfile;
    pid_t pid;
#ifdef CONFIG_VHOST_USER
    /* vhost user */
    VhostUserState *vhost_user;
    VHostNetState *vhost_net;
    CharFrontend vhost_chr;
    guint vhost_watch;
    uint64_t acked_features;
    bool started;
#endif
} NetPasstState;

static int net_passt_stream_start(NetPasstState *s, Error **errp);

static void net_passt_cleanup(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

#ifdef CONFIG_VHOST_USER
    if (s->vhost_net) {
        vhost_net_cleanup(s->vhost_net);
        g_free(s->vhost_net);
        s->vhost_net = NULL;
    }
    g_clear_handle_id(&s->vhost_watch, g_source_remove);
    qemu_chr_fe_deinit(&s->vhost_chr, true);
    if (s->vhost_user) {
        vhost_user_cleanup(s->vhost_user);
        g_free(s->vhost_user);
        s->vhost_user = NULL;
    }
#endif

    if (s->pid > 0) {
        kill(s->pid, SIGTERM);
    }
    if (g_remove(s->pidfile) != 0) {
        warn_report("Failed to remove passt pidfile %s: %s",
                    s->pidfile, strerror(errno));
    }
    g_free(s->pidfile);
    g_ptr_array_free(s->args, TRUE);
}

static ssize_t net_passt_receive(NetClientState *nc, const uint8_t *buf,
                                  size_t size)
{
    NetStreamData *d = DO_UPCAST(NetStreamData, nc, nc);

    return net_stream_data_receive(d, buf, size);
}

static gboolean net_passt_send(QIOChannel *ioc, GIOCondition condition,
                                gpointer data)
{
    if (net_stream_data_send(ioc, condition, data) == G_SOURCE_REMOVE) {
        NetPasstState *s = DO_UPCAST(NetPasstState, data, data);
        Error *error = NULL;

        /* we need to restart passt */
        kill(s->pid, SIGTERM);
        if (net_passt_stream_start(s, &error) == -1) {
            error_report_err(error);
        }

        return G_SOURCE_REMOVE;
    }

    return G_SOURCE_CONTINUE;
}

#ifdef CONFIG_VHOST_USER
static int passt_set_vnet_endianness(NetClientState *nc, bool enable)
{
    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    return 0;
}

static bool passt_has_vnet_hdr(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    return s->vhost_user != NULL;
}

static bool passt_has_ufo(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    return s->vhost_user != NULL;
}

static bool passt_check_peer_type(NetClientState *nc, ObjectClass *oc,
                                             Error **errp)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);
    const char *driver = object_class_get_name(oc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    if (s->vhost_user == NULL) {
        return true;
    }

    if (!g_str_has_prefix(driver, "virtio-net-")) {
        error_setg(errp, "vhost-user requires frontend driver virtio-net-*");
        return false;
    }

    return true;
}

static struct vhost_net *passt_get_vhost_net(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    return s->vhost_net;
}

static uint64_t passt_get_acked_features(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    return s->acked_features;
}

static void passt_save_acked_features(NetClientState *nc)
{
    NetPasstState *s = DO_UPCAST(NetPasstState, data.nc, nc);

    assert(nc->info->type == NET_CLIENT_DRIVER_PASST);

    if (s->vhost_net) {
        uint64_t features = vhost_net_get_acked_features(s->vhost_net);
        if (features) {
            s->acked_features = features;
        }
    }
}
#endif

static NetClientInfo net_passt_info = {
    .type = NET_CLIENT_DRIVER_PASST,
    .size = sizeof(NetPasstState),
    .receive = net_passt_receive,
    .cleanup = net_passt_cleanup,
#ifdef CONFIG_VHOST_USER
    .has_vnet_hdr = passt_has_vnet_hdr,
    .has_ufo = passt_has_ufo,
    .set_vnet_be = passt_set_vnet_endianness,
    .set_vnet_le = passt_set_vnet_endianness,
    .check_peer_type = passt_check_peer_type,
    .get_vhost_net = passt_get_vhost_net,
#endif
};

static void net_passt_client_connected(QIOTask *task, gpointer opaque)
{
    NetPasstState *s = opaque;

    if (net_stream_data_client_connected(task, &s->data) == 0) {
        qemu_set_info_str(&s->data.nc, "stream,connected to pid %d", s->pid);
    }
}

static int net_passt_start_daemon(NetPasstState *s, int sock, Error **errp)
{
    g_autoptr(GSubprocess) daemon = NULL;
    g_autofree gchar *contents = NULL;
    g_autoptr(GError) error = NULL;
    GSubprocessLauncher *launcher;

    qemu_set_info_str(&s->data.nc, "launching passt");

    launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);
    g_subprocess_launcher_take_fd(launcher, sock, 3);

    daemon =  g_subprocess_launcher_spawnv(launcher,
                                           (const gchar *const *)s->args->pdata,
                                           &error);
    g_object_unref(launcher);

    if (!daemon) {
        error_setg(errp, "Error creating daemon: %s", error->message);
        return -1;
    }

    if (!g_subprocess_wait(daemon, NULL, &error)) {
        error_setg(errp, "Error waiting for daemon: %s", error->message);
        return -1;
    }

    if (g_subprocess_get_if_exited(daemon)) {
        gint status = g_subprocess_get_exit_status(daemon);
        if (status) {
            error_setg(errp, "Passt exited with code %d", status);
            return -1;
        }
    }

    if (g_subprocess_get_if_signaled(daemon)) {
        error_setg(errp, "Passt killed with signal %d",
                   g_subprocess_get_term_sig(daemon));
        return -1;
    }

    if (!g_file_get_contents(s->pidfile, &contents, NULL, &error)) {
        error_setg(errp, "Cannot read passt pid: %s", error->message);
        return -1;
    }

    s->pid = (pid_t)g_ascii_strtoll(contents, NULL, 10);
    if (s->pid <= 0) {
        error_setg(errp, "File '%s' did not contain a valid PID.", s->pidfile);
        return -1;
    }

    return 0;
}

static int net_passt_stream_start(NetPasstState *s, Error **errp)
{
    QIOChannelSocket *sioc;
    SocketAddress *addr;
    int sv[2];

    if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        error_setg_errno(errp, errno, "socketpair() failed");
        return -1;
    }

    /* connect to passt */
    qemu_set_info_str(&s->data.nc, "connecting to passt");

    /* create socket channel */
    sioc = qio_channel_socket_new();
    s->data.ioc = QIO_CHANNEL(sioc);
    s->data.nc.link_down = true;
    s->data.send = net_passt_send;

    addr = g_new0(SocketAddress, 1);
    addr->type = SOCKET_ADDRESS_TYPE_FD;
    addr->u.fd.str = g_strdup_printf("%d", sv[0]);

    qio_channel_socket_connect_async(sioc, addr,
                                     net_passt_client_connected, s,
                                     NULL, NULL);

    qapi_free_SocketAddress(addr);

    /* start passt */
    if (net_passt_start_daemon(s, sv[1], errp) == -1) {
        close(sv[0]);
        close(sv[1]);
        return -1;
    }
    close(sv[1]);

    return 0;
}

#ifdef CONFIG_VHOST_USER
static gboolean passt_vhost_user_watch(void *do_not_use, GIOCondition cond,
                                       void *opaque)
{
    NetPasstState *s = opaque;

    qemu_chr_fe_disconnect(&s->vhost_chr);

    return G_SOURCE_CONTINUE;
}

static void passt_vhost_user_event(void *opaque, QEMUChrEvent event);

static void chr_closed_bh(void *opaque)
{
    NetPasstState *s = opaque;

    passt_save_acked_features(&s->data.nc);

    net_client_set_link(&(NetClientState *){ &s->data.nc }, 1, false);

    qemu_chr_fe_set_handlers(&s->vhost_chr, NULL, NULL, passt_vhost_user_event,
                             NULL, s, NULL, true);
}

static void passt_vhost_user_stop(NetPasstState *s)
{
    passt_save_acked_features(&s->data.nc);
    vhost_net_cleanup(s->vhost_net);
}

static int passt_vhost_user_start(NetPasstState *s, VhostUserState *be)
{
    struct vhost_net *net = NULL;
    VhostNetOptions options;

    options.backend_type = VHOST_BACKEND_TYPE_USER;
    options.net_backend = &s->data.nc;
    options.opaque = be;
    options.busyloop_timeout = 0;
    options.nvqs = 2;
    options.feature_bits = user_feature_bits;
    options.max_tx_queue_size = VIRTQUEUE_MAX_SIZE;
    options.get_acked_features = passt_get_acked_features;
    options.save_acked_features = passt_save_acked_features;
    options.is_vhost_user = true;

    net = vhost_net_init(&options);
    if (!net) {
        error_report("failed to init passt vhost_net");
        passt_vhost_user_stop(s);
        return -1;
    }

    if (s->vhost_net) {
        vhost_net_cleanup(s->vhost_net);
        g_free(s->vhost_net);
    }
    s->vhost_net = net;

    return 0;
}

static void passt_vhost_user_event(void *opaque, QEMUChrEvent event)
{
    NetPasstState *s = opaque;

    switch (event) {
    case CHR_EVENT_OPENED:
        if (passt_vhost_user_start(s, s->vhost_user) < 0) {
            qemu_chr_fe_disconnect(&s->vhost_chr);
            return;
        }
        s->vhost_watch = qemu_chr_fe_add_watch(&s->vhost_chr, G_IO_HUP,
                                               passt_vhost_user_watch, s);
        net_client_set_link(&(NetClientState *){ &s->data.nc }, 1, true);
        s->started = true;
        break;
    case CHR_EVENT_CLOSED:
        if (s->vhost_watch) {
            AioContext *ctx = qemu_get_current_aio_context();

            g_clear_handle_id(&s->vhost_watch, g_source_remove);
            qemu_chr_fe_set_handlers(&s->vhost_chr, NULL, NULL,  NULL, NULL,
                                     NULL, NULL, false);

            aio_bh_schedule_oneshot(ctx, chr_closed_bh, s);
        }
        break;
    case CHR_EVENT_BREAK:
    case CHR_EVENT_MUX_IN:
    case CHR_EVENT_MUX_OUT:
        /* Ignore */
        break;
    }
}

static int net_passt_vhost_user_init(NetPasstState *s, Error **errp)
{
    Chardev *chr;
    int sv[2];

    if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        error_setg_errno(errp, errno, "socketpair() failed");
        return -1;
    }

    /* connect to passt */
    qemu_set_info_str(&s->data.nc, "connecting to passt");

    /* create chardev */

    chr = CHARDEV(object_new(TYPE_CHARDEV_SOCKET));
    if (!chr || qemu_chr_add_client(chr, sv[0]) == -1) {
        object_unref(OBJECT(chr));
        error_setg(errp, "Failed to make socket chardev");
        goto err;
    }

    s->vhost_user = g_new0(struct VhostUserState, 1);
    if (!qemu_chr_fe_init(&s->vhost_chr, chr, errp) ||
        !vhost_user_init(s->vhost_user, &s->vhost_chr, errp)) {
        goto err;
    }

    /* start passt */
    if (net_passt_start_daemon(s, sv[1], errp) == -1) {
        goto err;
    }

    do {
        if (qemu_chr_fe_wait_connected(&s->vhost_chr, errp) < 0) {
            goto err;
        }

        qemu_chr_fe_set_handlers(&s->vhost_chr, NULL, NULL,
                                 passt_vhost_user_event, NULL, s, NULL,
                                 true);
    } while (!s->started);

    qemu_set_info_str(&s->data.nc, "vhost-user,connected to pid %d", s->pid);

    close(sv[1]);
    return 0;
err:
    close(sv[0]);
    close(sv[1]);

    return -1;
}
#else
static int net_passt_vhost_user_init(NetPasstState *s, Error **errp)
{
    error_setg(errp, "vhost-user support has not been built");

    return -1;
}
#endif

static GPtrArray *net_passt_decode_args(const NetdevPasstOptions *passt,
                                        gchar *pidfile, Error **errp)
{
    GPtrArray *args = g_ptr_array_new_with_free_func(g_free);

    if (passt->path) {
        g_ptr_array_add(args, g_strdup(passt->path));
    } else {
        g_ptr_array_add(args, g_strdup("passt"));
    }

    if (passt->has_vhost_user && passt->vhost_user) {
        g_ptr_array_add(args, g_strdup("--vhost-user"));
    }

    /* by default, be quiet */
    if (!passt->has_quiet || passt->quiet) {
        g_ptr_array_add(args, g_strdup("--quiet"));
    }

    if (passt->has_mtu) {
        g_ptr_array_add(args, g_strdup("--mtu"));
        g_ptr_array_add(args, g_strdup_printf("%"PRId64, passt->mtu));
    }

    if (passt->address) {
        g_ptr_array_add(args, g_strdup("--address"));
        g_ptr_array_add(args, g_strdup(passt->address));
    }

    if (passt->netmask) {
        g_ptr_array_add(args, g_strdup("--netmask"));
        g_ptr_array_add(args, g_strdup(passt->netmask));
    }

    if (passt->mac) {
        g_ptr_array_add(args, g_strdup("--mac-addr"));
        g_ptr_array_add(args, g_strdup(passt->mac));
    }

    if (passt->gateway) {
        g_ptr_array_add(args, g_strdup("--gateway"));
        g_ptr_array_add(args, g_strdup(passt->gateway));
    }

    if (passt->interface) {
        g_ptr_array_add(args, g_strdup("--interface"));
        g_ptr_array_add(args, g_strdup(passt->interface));
    }

    if (passt->outbound) {
        g_ptr_array_add(args, g_strdup("--outbound"));
        g_ptr_array_add(args, g_strdup(passt->outbound));
    }

    if (passt->outbound_if4) {
        g_ptr_array_add(args, g_strdup("--outbound-if4"));
        g_ptr_array_add(args, g_strdup(passt->outbound_if4));
    }

    if (passt->outbound_if6) {
        g_ptr_array_add(args, g_strdup("--outbound-if6"));
        g_ptr_array_add(args, g_strdup(passt->outbound_if6));
    }

    if (passt->dns) {
        g_ptr_array_add(args, g_strdup("--dns"));
        g_ptr_array_add(args, g_strdup(passt->dns));
    }
    if (passt->fqdn) {
        g_ptr_array_add(args, g_strdup("--fqdn"));
        g_ptr_array_add(args, g_strdup(passt->fqdn));
    }

    if (passt->has_dhcp_dns && !passt->dhcp_dns) {
        g_ptr_array_add(args, g_strdup("--no-dhcp-dns"));
    }

    if (passt->has_dhcp_search && !passt->dhcp_search) {
        g_ptr_array_add(args, g_strdup("--no-dhcp-search"));
    }

    if (passt->map_host_loopback) {
        g_ptr_array_add(args, g_strdup("--map-host-loopback"));
        g_ptr_array_add(args, g_strdup(passt->map_host_loopback));
    }

    if (passt->map_guest_addr) {
        g_ptr_array_add(args, g_strdup("--map-guest-addr"));
        g_ptr_array_add(args, g_strdup(passt->map_guest_addr));
    }

    if (passt->dns_forward) {
        g_ptr_array_add(args, g_strdup("--dns-forward"));
        g_ptr_array_add(args, g_strdup(passt->dns_forward));
    }

    if (passt->dns_host) {
        g_ptr_array_add(args, g_strdup("--dns-host"));
        g_ptr_array_add(args, g_strdup(passt->dns_host));
    }

    if (passt->has_tcp && !passt->tcp) {
        g_ptr_array_add(args, g_strdup("--no-tcp"));
    }

    if (passt->has_udp && !passt->udp) {
        g_ptr_array_add(args, g_strdup("--no-udp"));
    }

    if (passt->has_icmp && !passt->icmp) {
        g_ptr_array_add(args, g_strdup("--no-icmp"));
    }

    if (passt->has_dhcp && !passt->dhcp) {
        g_ptr_array_add(args, g_strdup("--no-dhcp"));
    }

    if (passt->has_ndp && !passt->ndp) {
        g_ptr_array_add(args, g_strdup("--no-ndp"));
    }
    if (passt->has_dhcpv6 && !passt->dhcpv6) {
        g_ptr_array_add(args, g_strdup("--no-dhcpv6"));
    }

    if (passt->has_ra && !passt->ra) {
        g_ptr_array_add(args, g_strdup("--no-ra"));
    }

    if (passt->has_freebind && passt->freebind) {
        g_ptr_array_add(args, g_strdup("--freebind"));
    }

    if (passt->has_ipv4 && !passt->ipv4) {
        g_ptr_array_add(args, g_strdup("--ipv6-only"));
    }

    if (passt->has_ipv6 && !passt->ipv6) {
        g_ptr_array_add(args, g_strdup("--ipv4-only"));
    }

    if (passt->has_search && passt->search) {
        const PasstSearchList *list = passt->search;
        GString *domains = g_string_new(list->value->str);

        list = list->next;
        while (list) {
            g_string_append(domains, " ");
            g_string_append(domains, list->value->str);
            list = list->next;
        }

        g_ptr_array_add(args, g_strdup("--search"));
        g_ptr_array_add(args, g_string_free(domains, FALSE));
    }

    if (passt->has_tcp_ports && passt->tcp_ports) {
        const PasstPortForwardList *list = passt->tcp_ports;
        GString *tcp_ports = g_string_new(list->value->str);

        list = list->next;
        while (list) {
            g_string_append(tcp_ports, ",");
            g_string_append(tcp_ports, list->value->str);
            list = list->next;
        }

        g_ptr_array_add(args, g_strdup("--tcp-ports"));
        g_ptr_array_add(args, g_string_free(tcp_ports, FALSE));
    }

    if (passt->has_udp_ports && passt->udp_ports) {
        const PasstPortForwardList *list = passt->udp_ports;
        GString *udp_ports = g_string_new(list->value->str);

        list = list->next;
        while (list) {
            g_string_append(udp_ports, ",");
            g_string_append(udp_ports, list->value->str);
            list = list->next;
        }

        g_ptr_array_add(args, g_strdup("--udp-ports"));
        g_ptr_array_add(args, g_string_free(udp_ports, FALSE));
    }

    if (passt->has_param && passt->param) {
        const PasstParameterList *list = passt->param;

        while (list) {
            g_ptr_array_add(args, g_strdup(list->value->str));
            list = list->next;
        }
    }

    /* provide a pid file to be able to kil passt on exit */
    g_ptr_array_add(args, g_strdup("--pid"));
    g_ptr_array_add(args, g_strdup(pidfile));

    /* g_subprocess_launcher_take_fd() will set the socket on fd 3 */
    g_ptr_array_add(args, g_strdup("--fd"));
    g_ptr_array_add(args, g_strdup("3"));

    g_ptr_array_add(args, NULL);

    return args;
}

int net_init_passt(const Netdev *netdev, const char *name,
                   NetClientState *peer, Error **errp)
{
    g_autoptr(GError) error = NULL;
    NetClientState *nc;
    NetPasstState *s;
    GPtrArray *args;
    gchar *pidfile;
    int pidfd;

    assert(netdev->type == NET_CLIENT_DRIVER_PASST);

    pidfd = g_file_open_tmp("passt-XXXXXX.pid", &pidfile, &error);
    if (pidfd == -1) {
        error_setg(errp, "Failed to create temporary file: %s", error->message);
        return -1;
    }
    close(pidfd);

    args = net_passt_decode_args(&netdev->u.passt, pidfile, errp);
    if (args == NULL) {
        g_free(pidfile);
        return -1;
    }

    nc = qemu_new_net_client(&net_passt_info, peer, "passt", name);
    s = DO_UPCAST(NetPasstState, data.nc, nc);

    s->args = args;
    s->pidfile = pidfile;

    if (netdev->u.passt.has_vhost_user && netdev->u.passt.vhost_user) {
        if (net_passt_vhost_user_init(s, errp) == -1) {
            qemu_del_net_client(nc);
            return -1;
        }

        return 0;
    }

    if (net_passt_stream_start(s, errp) == -1) {
        qemu_del_net_client(nc);
        return -1;
    }

    return 0;
}
