/*
 *  Copyright 2014 LKC Technologies, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <err.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <libmnl/libmnl.h>
#include <net/if_arp.h>
#include <net/if.h>
#include <net/route.h>
#include <linux/if.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

#include "erlcmd.h"

//#define DEBUG
#ifdef DEBUG
#define debug(...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\r\n"); } while(0)
#else
#define debug(...)
#endif

struct netif {
    // NETLINK_ROUTE socket information
    struct mnl_socket *nl;
    int seq;

    // NETLINK_KOBJECT_UEVENT socket information
    struct mnl_socket *nl_uevent;

    // AF_INET socket for ioctls
    int inet_fd;

    // Netlink buffering
    char nlbuf[8192]; // See MNL_SOCKET_BUFFER_SIZE

    // Erlang request processing
    const char *req;
    int req_index;

    // Erlang response processing
    char resp[ERLCMD_BUF_SIZE];
    int resp_index;

    // Async response handling
    void (*response_callback)(struct netif *nb, int bytecount);
    void (*response_error_callback)(struct netif *nb, int err);
    unsigned int response_portid;
    int response_seq;

    // Holder of the most recently encounted errno.
    int last_error;
};

static void netif_init(struct netif *nb)
{
    memset(nb, 0, sizeof(*nb));
    nb->nl = mnl_socket_open(NETLINK_ROUTE);
    if (!nb->nl)
        err(EXIT_FAILURE, "mnl_socket_open (NETLINK_ROUTE)");

    if (mnl_socket_bind(nb->nl, RTMGRP_LINK, MNL_SOCKET_AUTOPID) < 0)
        err(EXIT_FAILURE, "mnl_socket_bind");

    nb->nl_uevent = mnl_socket_open(NETLINK_KOBJECT_UEVENT);
    if (!nb->nl_uevent)
        err(EXIT_FAILURE, "mnl_socket_open (NETLINK_KOBJECT_UEVENT)");

    // There is one single group in kobject over netlink
    if (mnl_socket_bind(nb->nl_uevent, (1<<0), MNL_SOCKET_AUTOPID) < 0)
        err(EXIT_FAILURE, "mnl_socket_bind");

    nb->inet_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (nb->inet_fd < 0)
        err(EXIT_FAILURE, "socket");

    nb->seq = 1;
}

static void netif_cleanup(struct netif *nb)
{
    mnl_socket_close(nb->nl);
    mnl_socket_close(nb->nl_uevent);
    nb->nl = NULL;
}

static void start_response(struct netif *nb)
{
    nb->resp_index = sizeof(uint16_t); // Space for payload size
    nb->resp[nb->resp_index++] = 'r'; // Indicate response
    ei_encode_version(nb->resp, &nb->resp_index);
}

static void send_response(struct netif *nb)
{
    debug("sending response: %d bytes", nb->resp_index);
    erlcmd_send(nb->resp, nb->resp_index);
    nb->resp_index = 0;
}

static int collect_if_attrs(const struct nlattr *attr, void *data)
{
    const struct nlattr **tb = data;
    int type = mnl_attr_get_type(attr);

    // Skip unsupported attributes in user-space
    if (mnl_attr_type_valid(attr, IFLA_MAX) < 0)
        return MNL_CB_OK;

    // Only save supported attributes (see encode logic)
    switch (type) {
    case IFLA_MTU:
    case IFLA_IFNAME:
    case IFLA_ADDRESS:
    case IFLA_BROADCAST:
    case IFLA_LINK:
    case IFLA_OPERSTATE:
    case IFLA_STATS:
        tb[type] = attr;
        break;

    default:
        break;
    }
    return MNL_CB_OK;
}

static void encode_kv_long(struct netif *nb, const char *key, long value)
{
    ei_encode_atom(nb->resp, &nb->resp_index, key);
    ei_encode_long(nb->resp, &nb->resp_index, value);
}

static void encode_kv_ulong(struct netif *nb, const char *key, unsigned long value)
{
    ei_encode_atom(nb->resp, &nb->resp_index, key);
    ei_encode_ulong(nb->resp, &nb->resp_index, value);
}
static void encode_kv_bool(struct netif *nb, const char *key, int value)
{
    ei_encode_atom(nb->resp, &nb->resp_index, key);
    ei_encode_boolean(nb->resp, &nb->resp_index, value);
}
static void encode_string(char *buf, int *index, const char *str)
{
    // Encode strings as binaries so that we get Elixir strings
    // NOTE: the strings that we encounter here are expected to be ASCII to
    //       my knowledge
    ei_encode_binary(buf, index, str, strlen(str));
}
static void encode_kv_string(struct netif *nb, const char *key, const char *str)
{
    ei_encode_atom(nb->resp, &nb->resp_index, key);
    encode_string(nb->resp, &nb->resp_index, str);
}

static void encode_kv_stats(struct netif *nb, const char *key, struct nlattr *attr)
{
    struct rtnl_link_stats *stats = (struct rtnl_link_stats *) mnl_attr_get_payload(attr);

    ei_encode_atom(nb->resp, &nb->resp_index, key);
    ei_encode_map_header(nb->resp, &nb->resp_index, 10);
    encode_kv_ulong(nb, "rx_packets", stats->rx_packets);
    encode_kv_ulong(nb, "tx_packets", stats->tx_packets);
    encode_kv_ulong(nb, "rx_bytes", stats->rx_bytes);
    encode_kv_ulong(nb, "tx_bytes", stats->tx_bytes);
    encode_kv_ulong(nb, "rx_errors", stats->rx_errors);
    encode_kv_ulong(nb, "tx_errors", stats->tx_errors);
    encode_kv_ulong(nb, "rx_dropped", stats->rx_dropped);
    encode_kv_ulong(nb, "tx_dropped", stats->tx_dropped);
    encode_kv_ulong(nb, "multicast", stats->multicast);
    encode_kv_ulong(nb, "collisions", stats->collisions);
}

static void encode_kv_operstate(struct netif *nb, int operstate)
{
    ei_encode_atom(nb->resp, &nb->resp_index, "operstate");

    // Refer to RFC2863 for state descriptions (or the kernel docs)
    const char *operstate_atom;
    switch (operstate) {
    default:
    case IF_OPER_UNKNOWN:        operstate_atom = "unknown"; break;
    case IF_OPER_NOTPRESENT:     operstate_atom = "notpresent"; break;
    case IF_OPER_DOWN:           operstate_atom = "down"; break;
    case IF_OPER_LOWERLAYERDOWN: operstate_atom = "lowerlayerdown"; break;
    case IF_OPER_TESTING:        operstate_atom = "testing"; break;
    case IF_OPER_DORMANT:        operstate_atom = "dormant"; break;
    case IF_OPER_UP:             operstate_atom = "up"; break;
    }
    ei_encode_atom(nb->resp, &nb->resp_index, operstate_atom);
}

static int netif_build_ifinfo(const struct nlmsghdr *nlh, void *data)
{
    struct netif *nb = (struct netif *) data;
    struct nlattr *tb[IFLA_MAX + 1];
    memset(tb, 0, sizeof(tb));
    struct ifinfomsg *ifm = mnl_nlmsg_get_payload(nlh);

    if (mnl_attr_parse(nlh, sizeof(*ifm), collect_if_attrs, tb) != MNL_CB_OK) {
        debug("Error from mnl_attr_parse");
        return MNL_CB_ERROR;
    }

    int count = 7; // Number of fields that we always encode
    int i;
    for (i = 0; i <= IFLA_MAX; i++)
        if (tb[i])
            count++;

    ei_encode_map_header(nb->resp, &nb->resp_index, count);

    encode_kv_long(nb, "index", ifm->ifi_index);

    ei_encode_atom(nb->resp, &nb->resp_index, "type");
    ei_encode_atom(nb->resp, &nb->resp_index, ifm->ifi_type == ARPHRD_ETHER ? "ethernet" : "other");

    encode_kv_bool(nb, "is_up", ifm->ifi_flags & IFF_UP);
    encode_kv_bool(nb, "is_broadcast", ifm->ifi_flags & IFF_BROADCAST);
    encode_kv_bool(nb, "is_running", ifm->ifi_flags & IFF_RUNNING);
    encode_kv_bool(nb, "is_lower_up", ifm->ifi_flags & IFF_LOWER_UP);
    encode_kv_bool(nb, "is_multicast", ifm->ifi_flags & IFF_MULTICAST);

    if (tb[IFLA_MTU])
        encode_kv_ulong(nb, "mtu", mnl_attr_get_u32(tb[IFLA_MTU]));
    if (tb[IFLA_IFNAME])
        encode_kv_string(nb, "ifname", mnl_attr_get_str(tb[IFLA_IFNAME]));
    if (tb[IFLA_ADDRESS]) {
        ei_encode_atom(nb->resp, &nb->resp_index, "mac_address");
        ei_encode_binary(nb->resp, &nb->resp_index, mnl_attr_get_payload(tb[IFLA_ADDRESS]), mnl_attr_get_payload_len(tb[IFLA_ADDRESS]));
    }
    if (tb[IFLA_BROADCAST]) {
        ei_encode_atom(nb->resp, &nb->resp_index, "mac_broadcast");
        ei_encode_binary(nb->resp, &nb->resp_index, mnl_attr_get_payload(tb[IFLA_BROADCAST]), mnl_attr_get_payload_len(tb[IFLA_BROADCAST]));
    }
    if (tb[IFLA_LINK])
        encode_kv_ulong(nb, "link", mnl_attr_get_u32(tb[IFLA_LINK]));
    if (tb[IFLA_OPERSTATE])
        encode_kv_operstate(nb, mnl_attr_get_u32(tb[IFLA_OPERSTATE]));
    if (tb[IFLA_STATS])
        encode_kv_stats(nb, "stats", tb[IFLA_STATS]);

    return MNL_CB_OK;
}

static void nl_uevent_process(struct netif *nb)
{
    int bytecount = mnl_socket_recvfrom(nb->nl_uevent, nb->nlbuf, sizeof(nb->nlbuf));
    if (bytecount <= 0)
        err(EXIT_FAILURE, "mnl_socket_recvfrom");

    // uevent messages are concatenated strings
    enum hotplug_operation {
        HOTPLUG_OPERATION_NONE = 0,
        HOTPLUG_OPERATION_ADD,
        HOTPLUG_OPERATION_MOVE,
        HOTPLUG_OPERATION_REMOVE
    } operation = HOTPLUG_OPERATION_NONE;

    const char *str = nb->nlbuf;
    if (strncmp(str, "add@", 4) == 0)
        operation = HOTPLUG_OPERATION_ADD;
    else if (strncmp(str, "move@", 5) == 0)
        operation = HOTPLUG_OPERATION_MOVE;
    else if (strncmp(str, "remove@", 7) == 0)
        operation = HOTPLUG_OPERATION_REMOVE;
    else
        return; // Not interested in this message.

    const char *str_end = str + bytecount;
    str += strlen(str) + 1;

    // Extract the fields of interest
    const char *ifname = NULL;
    const char *subsystem = NULL;
    const char *ifindex = NULL;
    for (;str < str_end; str += strlen(str) + 1) {
        if (strncmp(str, "INTERFACE=", 10) == 0)
            ifname = str + 10;
        else if (strncmp(str, "SUBSYSTEM=", 10) == 0)
            subsystem = str + 10;
        else if (strncmp(str, "IFINDEX=", 8) == 0)
            ifindex = str + 8;
    }

    // Check that we have the required fields that this is a
    // "net" subsystem event. If yes, send the notification.
    if (ifname && subsystem && ifindex && strcmp(subsystem, "net") == 0) {
        nb->resp_index = sizeof(uint16_t); // Skip over payload size
        nb->resp[nb->resp_index++] = 'n';
        ei_encode_version(nb->resp, &nb->resp_index);

        ei_encode_tuple_header(nb->resp, &nb->resp_index, 2);

        switch (operation) {
        case HOTPLUG_OPERATION_ADD:
            ei_encode_atom(nb->resp, &nb->resp_index, "ifadded");
            break;
        case HOTPLUG_OPERATION_MOVE:
            ei_encode_atom(nb->resp, &nb->resp_index, "ifrenamed");
            break;
        case HOTPLUG_OPERATION_REMOVE:
        default: // Silence warning
            ei_encode_atom(nb->resp, &nb->resp_index, "ifremoved");
            break;
        }

        ei_encode_map_header(nb->resp, &nb->resp_index, 2);

        encode_kv_long(nb, "index", strtol(ifindex, NULL, 0));
        encode_kv_string(nb, "ifname", ifname);

        erlcmd_send(nb->resp, nb->resp_index);
    }
}

static void handle_notification(struct netif *nb, int bytecount)
{
    // Create the notification
    nb->resp_index = sizeof(uint16_t); // Skip over payload size
    nb->resp[nb->resp_index++] = 'n';
    ei_encode_version(nb->resp, &nb->resp_index);

    ei_encode_tuple_header(nb->resp, &nb->resp_index, 2);

    // Currently, the only notifications are interface changes.
    ei_encode_atom(nb->resp, &nb->resp_index, "ifchanged");
    if (mnl_cb_run(nb->nlbuf, bytecount, 0, 0, netif_build_ifinfo, nb) <= 0)
        err(EXIT_FAILURE, "mnl_cb_run");

    erlcmd_send(nb->resp, nb->resp_index);
}

static void handle_async_response(struct netif *nb, int bytecount)
{
    nb->response_callback(nb, bytecount);
    nb->response_callback = NULL;
    nb->response_error_callback = NULL;
}

static void handle_async_response_error(struct netif *nb, int err)
{
    nb->response_error_callback(nb, err);
    nb->response_callback = NULL;
    nb->response_error_callback = NULL;
}

static void nl_route_process(struct netif *nb)
{
    int bytecount = mnl_socket_recvfrom(nb->nl, nb->nlbuf, sizeof(nb->nlbuf));
    if (bytecount <= 0)
        err(EXIT_FAILURE, "mnl_socket_recvfrom");

    // Check if there's an async response on the saved sequence number and port id.
    // If not or if the message is not expected, then it's a notification.
    if (nb->response_callback != NULL) {
        int rc = mnl_cb_run(nb->nlbuf, bytecount, nb->response_seq, nb->response_portid, NULL, NULL);
        if (rc == MNL_CB_OK)
            handle_async_response(nb, bytecount);
        else if (rc == MNL_CB_ERROR && errno != ESRCH && errno != EPROTO)
            handle_async_response_error(nb, errno);
        else
            handle_notification(nb, bytecount);
    } else
        handle_notification(nb, bytecount);
}

static void netif_handle_interfaces(struct netif *nb)
{
    struct if_nameindex *if_ni = if_nameindex();
    if (if_ni == NULL)
        err(EXIT_FAILURE, "if_nameindex");

    start_response(nb);
    for (struct if_nameindex *i = if_ni;
         ! (i->if_index == 0 && i->if_name == NULL);
         i++) {
        debug("Found interface %s.", i->if_name);
        ei_encode_list_header(nb->resp, &nb->resp_index, 1);
        encode_string(nb->resp, &nb->resp_index, i->if_name);
    }

    if_freenameindex(if_ni);

    ei_encode_empty_list(nb->resp, &nb->resp_index);
    send_response(nb);
}

static void netif_handle_status_callback(struct netif *nb, int bytecount)
{
    start_response(nb);

    int original_resp_index = nb->resp_index;

    ei_encode_tuple_header(nb->resp, &nb->resp_index, 2);
    ei_encode_atom(nb->resp, &nb->resp_index, "ok");
    if (mnl_cb_run(nb->nlbuf, bytecount, nb->response_seq, nb->response_portid, netif_build_ifinfo, nb) < 0) {
        debug("error from or mnl_cb_run?");
        nb->resp_index = original_resp_index;
        erlcmd_encode_errno_error(nb->resp, &nb->resp_index, errno);
    }

    send_response(nb);
}

static void netif_handle_status_error_callback(struct netif *nb, int err)
{
    start_response(nb);
    erlcmd_encode_errno_error(nb->resp, &nb->resp_index, err);
    send_response(nb);
}

static void netif_handle_status(struct netif *nb,
                                    const char *ifname)
{
    struct nlmsghdr *nlh;
    struct ifinfomsg *ifi;
    unsigned int seq;

    nlh = mnl_nlmsg_put_header(nb->nlbuf);
    nlh->nlmsg_type = RTM_GETLINK;
    nlh->nlmsg_flags = NLM_F_REQUEST;
    nlh->nlmsg_seq = seq = nb->seq++;

    ifi = mnl_nlmsg_put_extra_header(nlh, sizeof(struct ifinfomsg));
    ifi->ifi_family = AF_UNSPEC;
    ifi->ifi_type = ARPHRD_ETHER;
    ifi->ifi_index = 0;
    ifi->ifi_flags = 0;
    ifi->ifi_change = 0xffffffff;

    mnl_attr_put_str(nlh, IFLA_IFNAME, ifname);

    if (mnl_socket_sendto(nb->nl, nlh, nlh->nlmsg_len) < 0)
        err(EXIT_FAILURE, "mnl_socket_send");

    nb->response_callback = netif_handle_status_callback;
    nb->response_error_callback = netif_handle_status_error_callback;
    nb->response_portid = mnl_socket_get_portid(nb->nl);
    nb->response_seq = seq;
}

static void netif_set_ifflags(struct netif *nb,
                                  const char *ifname,
                                  uint32_t flags,
                                  uint32_t mask)
{
    struct ifreq ifr;

    start_response(nb);

    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if (ioctl(nb->inet_fd, SIOCGIFFLAGS, &ifr) < 0) {
        debug("SIOCGIFFLAGS error: %s", strerror(errno));
        erlcmd_encode_errno_error(nb->resp, &nb->resp_index, errno);
        send_response(nb);
        return;
    }

    if ((ifr.ifr_flags ^ flags) & mask) {
        ifr.ifr_flags = (ifr.ifr_flags & ~mask) | (mask & flags);
        if (ioctl(nb->inet_fd, SIOCSIFFLAGS, &ifr)) {
            debug("SIOCGIFFLAGS error: %s", strerror(errno));
            erlcmd_encode_errno_error(nb->resp, &nb->resp_index, errno);
            send_response(nb);
            return;
        }
    }
    erlcmd_encode_ok(nb->resp, &nb->resp_index);
    send_response(nb);
}

static int collect_route_attrs(const struct nlattr *attr, void *data)
{
    const struct nlattr **tb = data;
    int type = mnl_attr_get_type(attr);

    // Skip unsupported attributes in user-space
    if (mnl_attr_type_valid(attr, RTA_MAX) < 0)
        return MNL_CB_OK;

    tb[type] = attr;
    return MNL_CB_OK;
}

struct fdg_data {
    int oif;
    char *result;
};

static int check_default_gateway(const struct nlmsghdr *nlh, void *data)
{
    struct nlattr *tb[RTA_MAX + 1];
    memset(tb, 0, sizeof(tb));
    struct rtmsg *rm = mnl_nlmsg_get_payload(nlh);
    mnl_attr_parse(nlh, sizeof(*rm), collect_route_attrs, tb);

    struct fdg_data *fdg = (struct fdg_data *) data;
    if (rm->rtm_scope == 0 &&
            tb[RTA_OIF] &&
            (int) mnl_attr_get_u32(tb[RTA_OIF]) == fdg->oif &&
            tb[RTA_GATEWAY]) {
        // Found it.
        inet_ntop(AF_INET, mnl_attr_get_payload(tb[RTA_GATEWAY]), fdg->result, INET_ADDRSTRLEN);
    }

    return MNL_CB_OK;
}

static void find_default_gateway(struct netif *nb,
                                int oif,
                                char *result)
{
    struct nlmsghdr *nlh;
    struct rtmsg *rtm;
    unsigned int seq;

    nlh = mnl_nlmsg_put_header(nb->nlbuf);
    nlh->nlmsg_type = RTM_GETROUTE;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    nlh->nlmsg_seq = seq = nb->seq++;

    rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(struct rtmsg));
    rtm->rtm_family = AF_INET;

    if (mnl_socket_sendto(nb->nl, nlh, nlh->nlmsg_len) < 0)
        err(EXIT_FAILURE, "mnl_socket_send");

    unsigned int portid = mnl_socket_get_portid(nb->nl);

    struct fdg_data fdg;
    fdg.oif = oif;
    fdg.result = result;
    *fdg.result = '\0';

    ssize_t ret = mnl_socket_recvfrom(nb->nl, nb->nlbuf, sizeof(nb->nlbuf));
    while (ret > 0) {
        ret = mnl_cb_run(nb->nlbuf, ret, seq, portid, check_default_gateway, &fdg);
        if (ret <= MNL_CB_STOP)
            break;
        ret = mnl_socket_recvfrom(nb->nl, nb->nlbuf, sizeof(nb->nlbuf));
    }
    if (ret == -1)
        err(EXIT_FAILURE, "mnl_socket_recvfrom");
}

struct ip_setting_handler {
    const char *name;
    int (*prep)(const struct ip_setting_handler *handler, struct netif *nb, void **context);
    int (*set)(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname, void *context);
    int (*get)(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname);

    // data for handlers
    int ioctl_set;
    int ioctl_get;
};

static int prep_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, void **context);
static int set_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname, void *context);
static int get_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname);
static int prep_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, void **context);
static int set_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname, void *context);
static int get_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname);

// These handlers are listed in the order that they should be invoked when
// configuring the interface. For example, "ipv4_gateway" is listed at the end
// so that it is set after the address and subnet_mask. If this is not done,
// setting the gateway may fail since Linux thinks that it is on the wrong subnet.
static const struct ip_setting_handler handlers[] = {
    { "ipv4_address", prep_ipaddr_ioctl, set_ipaddr_ioctl, get_ipaddr_ioctl, SIOCSIFADDR, SIOCGIFADDR },
    { "ipv4_subnet_mask", prep_ipaddr_ioctl, set_ipaddr_ioctl, get_ipaddr_ioctl, SIOCSIFNETMASK, SIOCGIFNETMASK },
    { "ipv4_broadcast", prep_ipaddr_ioctl, set_ipaddr_ioctl, get_ipaddr_ioctl, SIOCSIFBRDADDR, SIOCGIFBRDADDR },
    { "ipv4_gateway", prep_default_gateway, set_default_gateway, get_default_gateway, 0, 0 }
};
#define HANDLER_COUNT (sizeof(handlers) / sizeof(handlers[0]))

static int prep_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, void **context)
{
    char ipaddr[INET_ADDRSTRLEN];
    if (erlcmd_decode_string(nb->req, &nb->req_index, ipaddr, INET_ADDRSTRLEN) < 0)
        errx(EXIT_FAILURE, "ip address parameter required for '%s'", handler->name);

    // Be forgiving and if the user specifies an empty IP address, just skip
    // this request.
    if (ipaddr[0] == '\0')
        *context = NULL;
    else
        *context = strdup(ipaddr);

    return 0;
}

static int set_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname, void *context)
{
    const char *ipaddr = (const char *) context;

    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);

    struct sockaddr_in *addr = (struct sockaddr_in *) &ifr.ifr_addr;
    addr->sin_family = AF_INET;
    if (inet_pton(AF_INET, ipaddr, &addr->sin_addr) <= 0) {
        debug("Bad IP address for '%s': %s", handler->name, ipaddr);
        nb->last_error = EINVAL;
        return -1;
    }

    if (ioctl(nb->inet_fd, handler->ioctl_set, &ifr) < 0) {
        debug("ioctl(0x%04x) failed for setting '%s': %s", handler->ioctl_set, handler->name, strerror(errno));
        nb->last_error = errno;
        return -1;
    }

    return 0;
}

static int get_ipaddr_ioctl(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname)
{
    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);

    if (ioctl(nb->inet_fd, handler->ioctl_get, &ifr) < 0) {
        debug("ioctl(0x%04x) failed for getting '%s': %s", handler->ioctl_get, handler->name, strerror(errno));
        nb->last_error = errno;
        return -1;
    }

    struct sockaddr_in *addr = (struct sockaddr_in *) &ifr.ifr_addr;
    if (addr->sin_family == AF_INET) {
        char addrstr[INET_ADDRSTRLEN];
        if (!inet_ntop(addr->sin_family, &addr->sin_addr, addrstr, sizeof(addrstr))) {
            debug("inet_ntop failed for '%s'? : %s", handler->name, strerror(errno));
            nb->last_error = errno;
            return -1;
        }
        encode_kv_string(nb, handler->name, addrstr);
    } else {
        debug("got unexpected sin_family %d for '%s'", addr->sin_family, handler->name);
        nb->last_error = EINVAL;
        return -1;
    }
    return 0;
}

static int remove_all_gateways(struct netif *nb, const char *ifname)
{
    struct rtentry route;
    memset(&route, 0, sizeof(route));

    struct sockaddr_in *addr = (struct sockaddr_in *) &route.rt_gateway;
    addr->sin_family = AF_INET;
    addr->sin_addr.s_addr = INADDR_ANY;

    addr = (struct sockaddr_in*) &route.rt_dst;
    addr->sin_family = AF_INET;
    addr->sin_addr.s_addr = INADDR_ANY;

    addr = (struct sockaddr_in*) &route.rt_genmask;
    addr->sin_family = AF_INET;
    addr->sin_addr.s_addr = INADDR_ANY;

    route.rt_dev = (char *) ifname;
    route.rt_flags = RTF_GATEWAY;
    route.rt_metric = 0;

    // There may be more than one gateway. Remove all
    // of them.
    for (;;) {
        int rc = ioctl(nb->inet_fd, SIOCDELRT, &route);
        if (rc < 0) {
            if (errno == ESRCH) {
                return 0;
            } else {
                nb->last_error = errno;
                return -1;
            }
        }
    }
}

static int add_default_gateway(struct netif *nb, const char *ifname, const char *gateway_ip)
{
    struct rtentry route;
    memset(&route, 0, sizeof(route));

    struct sockaddr_in *addr = (struct sockaddr_in *)&route.rt_gateway;
    memset(addr, 0, sizeof(struct sockaddr_in));
    addr->sin_family = AF_INET;
    if (inet_pton(AF_INET, gateway_ip, &addr->sin_addr) <= 0) {
        debug("Bad IP address for the default gateway: %s", gateway_ip);
        nb->last_error = EINVAL;
        return -1;
    }

    addr = (struct sockaddr_in*) &route.rt_dst;
    memset(addr, 0, sizeof(struct sockaddr_in));
    addr->sin_family = AF_INET;
    addr->sin_addr.s_addr = INADDR_ANY;

    addr = (struct sockaddr_in*) &route.rt_genmask;
    memset(addr, 0, sizeof(struct sockaddr_in));
    addr->sin_family = AF_INET;
    addr->sin_addr.s_addr = INADDR_ANY;

    route.rt_dev = (char *) ifname;
    route.rt_flags = RTF_UP | RTF_GATEWAY;
    route.rt_metric = 0;

    int rc = ioctl(nb->inet_fd, SIOCADDRT, &route);
    if (rc < 0 && errno != EEXIST) {
        nb->last_error = errno;
        return -1;
    }
    return 0;
}

static int prep_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, void **context)
{
    char gateway[INET_ADDRSTRLEN];
    if (erlcmd_decode_string(nb->req, &nb->req_index, gateway, INET_ADDRSTRLEN) < 0)
        errx(EXIT_FAILURE, "ip address parameter required for '%s'", handler->name);

    *context = strdup(gateway);
    return 0;
}

static int set_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname, void *context)
{
    (void) handler;
    const char *gateway = context;

    // Before one can be set, any configured gateways need to be removed.
    if (remove_all_gateways(nb, ifname) < 0)
        return -1;

    // If no gateway was specified, then we're done.
    if (*gateway == '\0')
        return 0;

    return add_default_gateway(nb, ifname, gateway);
}

static int ifname_to_index(struct netif *nb, const char *ifname)
{
    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);

    if (ioctl(nb->inet_fd, SIOCGIFINDEX, &ifr) < 0) {
        nb->last_error = errno;
        return -1;
    }
    return ifr.ifr_ifindex;
}

static int get_default_gateway(const struct ip_setting_handler *handler, struct netif *nb, const char *ifname)
{
    int oif = ifname_to_index(nb, ifname);
    if (oif < 0)
        return -1;

    char gateway_ip[INET_ADDRSTRLEN];
    find_default_gateway(nb, oif, gateway_ip);

    // If the gateway isn't found, then the empty string is what we want.
    encode_kv_string(nb, handler->name, gateway_ip);
    return 0;
}

static void netif_handle_get(struct netif *nb,
                                 const char *ifname)
{
    start_response(nb);
    int original_resp_index = nb->resp_index;

    ei_encode_tuple_header(nb->resp, &nb->resp_index, 2);
    ei_encode_atom(nb->resp, &nb->resp_index, "ok");
    ei_encode_map_header(nb->resp, &nb->resp_index, HANDLER_COUNT);

    nb->last_error = 0;
    for (size_t i = 0; i < HANDLER_COUNT; i++) {
        const struct ip_setting_handler *handler = &handlers[i];
        if (handler->get(handler, nb, ifname) < 0)
            break;
    }

    if (nb->last_error) {
        nb->resp_index = original_resp_index;
        erlcmd_encode_errno_error(nb->resp, &nb->resp_index, nb->last_error);
    }
    send_response(nb);
}

static const struct ip_setting_handler *find_handler(const char *name)
{
    for (size_t i = 0; i < HANDLER_COUNT; i++) {
        const struct ip_setting_handler *handler = &handlers[i];
        if (strcmp(handler->name, name) == 0)
            return handler;
    }
    return NULL;
}

static void netif_handle_set(struct netif *nb,
                             const char *ifname)
{
    nb->last_error = 0;

    start_response(nb);

    int arity;
    if (ei_decode_map_header(nb->req, &nb->req_index, &arity) < 0)
        errx(EXIT_FAILURE, "setting attributes requires a map");

    void *handler_context[HANDLER_COUNT];
    memset(handler_context, 0, sizeof(handler_context));

    // Parse all options
    for (int i = 0; i < arity && nb->last_error == 0; i++) {
        char name[32];
        if (erlcmd_decode_atom(nb->req, &nb->req_index, name, sizeof(name)) < 0)
            errx(EXIT_FAILURE, "error in map encoding");

        // Look up the option. If we don't know it, silently ignore it so that
        // the caller can pass in maps that contain options for other code.
        const struct ip_setting_handler *handler = find_handler(name);
        if (handler)
            handler->prep(handler, nb, &handler_context[handler - handlers]);
        else
            ei_skip_term(nb->req, &nb->req_index);
    }

    // If no errors, then set everything
    if (!nb->last_error) {
        // Order is important: see note on handlers
        for (size_t i = 0; i < HANDLER_COUNT; i++) {
            if (handler_context[i]) {
                handlers[i].set(&handlers[i], nb, ifname, handler_context[i]);
                free(handler_context[i]);
            }
        }
    }

    // Encode and send the response
    if (nb->last_error)
        erlcmd_encode_errno_error(nb->resp, &nb->resp_index, nb->last_error);
    else
        erlcmd_encode_ok(nb->resp, &nb->resp_index);

    send_response(nb);
}

static void netif_request_handler(const char *req, void *cookie)
{
    struct netif *nb = (struct netif *) cookie;
    char ifname[IFNAMSIZ];

    // Commands are of the form {Command, Arguments}:
    // { atom(), term() }
    nb->req_index = sizeof(uint16_t);
    nb->req = req;
    if (ei_decode_version(nb->req, &nb->req_index, NULL) < 0)
        errx(EXIT_FAILURE, "Message version issue?");

    int arity;
    if (ei_decode_tuple_header(nb->req, &nb->req_index, &arity) < 0 ||
            arity != 2)
        errx(EXIT_FAILURE, "expecting {cmd, args} tuple");

    char cmd[MAXATOMLEN];
    if (ei_decode_atom(nb->req, &nb->req_index, cmd) < 0)
        errx(EXIT_FAILURE, "expecting command atom");

    if (strcmp(cmd, "interfaces") == 0) {
        debug("interfaces");
        netif_handle_interfaces(nb);
    } else if (strcmp(cmd, "status") == 0) {
        if (erlcmd_decode_string(nb->req, &nb->req_index, ifname, IFNAMSIZ) < 0)
            errx(EXIT_FAILURE, "status requires ifname");
        debug("ifinfo: %s", ifname);
        netif_handle_status(nb, ifname);
    } else if (strcmp(cmd, "ifup") == 0) {
        if (erlcmd_decode_string(nb->req, &nb->req_index, ifname, IFNAMSIZ) < 0)
            errx(EXIT_FAILURE, "ifup requires ifname");
        debug("ifup: %s", ifname);
        netif_set_ifflags(nb, ifname, IFF_UP, IFF_UP);
    } else if (strcmp(cmd, "ifdown") == 0) {
        if (erlcmd_decode_string(nb->req, &nb->req_index, ifname, IFNAMSIZ) < 0)
            errx(EXIT_FAILURE, "ifdown requires ifname");
        debug("ifdown: %s", ifname);
        netif_set_ifflags(nb, ifname, 0, IFF_UP);
    } else if (strcmp(cmd, "setup") == 0) {
        if (ei_decode_tuple_header(nb->req, &nb->req_index, &arity) < 0 ||
                arity != 2 ||
                erlcmd_decode_string(nb->req, &nb->req_index, ifname, IFNAMSIZ) < 0)
            errx(EXIT_FAILURE, "setup requires {ifname, parameters}");
        debug("set: %s", ifname);
        netif_handle_set(nb, ifname);
    } else if (strcmp(cmd, "settings") == 0) {
        if (erlcmd_decode_string(nb->req, &nb->req_index, ifname, IFNAMSIZ) < 0)
            errx(EXIT_FAILURE, "settings requires ifname");
        debug("get: %s", ifname);
        netif_handle_get(nb, ifname);
    } else
        errx(EXIT_FAILURE, "unknown command: %s", cmd);
}

int main(int argc, char *argv[])
{
    (void) argc;
    (void) argv;

    struct netif nb;
    netif_init(&nb);

    struct erlcmd handler;
    erlcmd_init(&handler, netif_request_handler, &nb);

    for (;;) {
        struct pollfd fdset[3];

        fdset[0].fd = STDIN_FILENO;
        fdset[0].events = POLLIN;
        fdset[0].revents = 0;

        fdset[1].fd = mnl_socket_get_fd(nb.nl);
        fdset[1].events = POLLIN;
        fdset[1].revents = 0;

        fdset[2].fd = mnl_socket_get_fd(nb.nl_uevent);
        fdset[2].events = POLLIN;
        fdset[2].revents = 0;

        int rc = poll(fdset, 3, -1);
        if (rc < 0) {
            // Retry if EINTR
            if (errno == EINTR)
                continue;

            err(EXIT_FAILURE, "poll");
        }

        if (fdset[0].revents & (POLLIN | POLLHUP))
            erlcmd_process(&handler);
        if (fdset[1].revents & (POLLIN | POLLHUP))
            nl_route_process(&nb);
        if (fdset[2].revents & (POLLIN | POLLHUP))
            nl_uevent_process(&nb);
    }

    netif_cleanup(&nb);
    return 0;
}
