/* -*- indent-tabs-mode: nil; -*- */
#define LOG_GROUP LOG_GROUP_NAT_SERVICE

#include "proxy.h"

#include <sys/types.h>          /* must come before linux/netlink */
#include <sys/socket.h>

#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

#include <errno.h>
#include <string.h>
#include <unistd.h>


static int rtmon_check_defaults(const void *buf, size_t len);


/**
 * Read IPv6 routing table - Linux rtnetlink version.
 *
 * XXX: TODO: To avoid re-reading the table we should subscribe to
 * updates by binding a monitoring NETLINK_ROUTE socket to
 * sockaddr_nl::nl_groups = RTMGRP_IPV6_ROUTE.
 *
 * But that will provide updates only.  Documentation is scarce, but
 * from what I've seen it seems that to get accurate routing info the
 * monitoring socket needs to be created first, then full routing
 * table requested (easier to do via spearate socket), then monitoring
 * socket polled for input.  The first update(s) of the monitoring
 * socket may happen before full table is returned, so we can't just
 * count the defaults, we need to keep track of their { oif, gw } to
 * correctly ignore updates that are reported via monitoring socket,
 * but that are already reflected in the full routing table returned
 * in response to our request.
 */
int
rtmon_get_defaults(void)
{
    int rtsock;
    ssize_t nsent, ssize;
    int ndefrts;

    char *buf = NULL;
    size_t bufsize;

    struct {
        struct nlmsghdr nh;
        struct rtmsg rtm;
        char attrbuf[512];
    } rtreq;

    memset(&rtreq, 0, sizeof(rtreq));
    rtreq.nh.nlmsg_type = RTM_GETROUTE;
    rtreq.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    rtreq.rtm.rtm_family = AF_INET6;
    rtreq.rtm.rtm_table = RT_TABLE_MAIN;
    rtreq.rtm.rtm_protocol = RTPROT_UNSPEC;

    rtreq.nh.nlmsg_len = NLMSG_SPACE(sizeof(rtreq.rtm));

    bufsize = 1024;
    ssize = bufsize;
    for (;;) {
        char *newbuf;
        int recverr;

        newbuf = (char *)realloc(buf, ssize);
        if (newbuf == NULL) {
            DPRINTF0(("rtmon: failed to %sallocate buffer\n",
                      buf == NULL ? "" : "re"));
            free(buf);
            return -1;
        }

        buf = newbuf;
        bufsize = ssize;

        /* it's easier to reopen than to flush */
        rtsock = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
        if (rtsock < 0) {
            DPRINTF0(("rtmon: failed to create netlink socket: %s", strerror(errno)));
            free(buf);
            return -1;
        }

        nsent = send(rtsock, &rtreq, rtreq.nh.nlmsg_len, 0);
        if (nsent < 0) {
            DPRINTF0(("rtmon: RTM_GETROUTE failed: %s", strerror(errno)));
            close (rtsock);
            free(buf);
            return -1;
        }

        ssize = recv(rtsock, buf, bufsize, MSG_TRUNC);
        recverr = errno;
        close (rtsock);

        if (ssize < 0) {
            DPRINTF(("rtmon: failed to read RTM_GETROUTE response: %s",
                     strerror(recverr)));
            free(buf);
            return -1;
        }

        if ((size_t)ssize <= bufsize) {
            DPRINTF2(("rtmon: RTM_GETROUTE: %lu bytes\n",
                      (unsigned long)ssize));
            break;
        }

        DPRINTF2(("rtmon: RTM_GETROUTE: truncated %lu to %lu bytes, retrying\n",
                  (unsigned long)ssize, (unsigned long)bufsize));
        /* try again with larger buffer */
    }

    ndefrts = rtmon_check_defaults(buf, (size_t)ssize);
    free(buf);

    if (ndefrts == 0) {
        DPRINTF(("rtmon: no IPv6 default routes found\n"));
    }
    else {
        DPRINTF(("rtmon: %d IPv6 default route%s found\n",
                 ndefrts,
                 ndefrts == 1 || ndefrts == -1 ? "" : "s"));
    }

    return ndefrts;
}


/**
 * Scan netlink message in the buffer for IPv6 default route changes.
 */
static int
rtmon_check_defaults(const void *buf, size_t len)
{
    struct nlmsghdr *nh;
    int dfltdiff = 0;

    for (nh = (struct nlmsghdr *)buf;
         NLMSG_OK(nh, len);
         nh = NLMSG_NEXT(nh, len))
    {
        struct rtmsg *rtm;
        struct rtattr *rta;
        int attrlen;
        int delta = 0;
        const void *gwbuf;
        size_t gwlen;
        int oif;

        DPRINTF2(("nlmsg type %d flags 0x%x\n",
                  nh->nlmsg_seq, nh->nlmsg_type, nh->nlmsg_flags));

        if (nh->nlmsg_type == NLMSG_DONE) {
            break;
        }

        if (nh->nlmsg_type == NLMSG_ERROR) {
            struct nlmsgerr *ne = (struct nlmsgerr *)NLMSG_DATA(nh);
            DPRINTF2(("> error %d\n", ne->error));
            LWIP_UNUSED_ARG(ne);
            break;
        }

        if (nh->nlmsg_type < RTM_BASE || RTM_MAX <= nh->nlmsg_type) {
            /* shouldn't happen */
            DPRINTF2(("> not an RTM message!\n"));
            continue;
        }


        rtm = (struct rtmsg *)NLMSG_DATA(nh);
        attrlen = RTM_PAYLOAD(nh);

        if (nh->nlmsg_type == RTM_NEWROUTE) {
            delta = +1;
        }
        else if (nh->nlmsg_type == RTM_DELROUTE) {
            delta = -1;
        }
        else {
            /* shouldn't happen */
            continue;
        }

        /*
         * Is this an IPv6 default route in the main table?  (Local
         * table always has ::/0 reject route, hence the last check).
         */
        if (rtm->rtm_family == AF_INET6 /* should always be true */
            && rtm->rtm_dst_len == 0
            && rtm->rtm_table == RT_TABLE_MAIN)
        {
            dfltdiff += delta;
        }
        else {
            /* some other route change */
            continue;
        }


        gwbuf = NULL;
        gwlen = 0;
        oif = -1;

        for (rta = RTM_RTA(rtm);
             RTA_OK(rta, attrlen);
             rta = RTA_NEXT(rta, attrlen))
        {
            if (rta->rta_type == RTA_GATEWAY) {
                gwbuf = RTA_DATA(rta);
                gwlen = RTA_PAYLOAD(rta);
            }
            else if (rta->rta_type == RTA_OIF) {
                /* assert RTA_PAYLOAD(rta) == 4 */
                memcpy(&oif, RTA_DATA(rta), sizeof(oif));
            }
        }

        /* XXX: TODO: note that { oif, gw } was added/removed */
        LWIP_UNUSED_ARG(gwbuf);
        LWIP_UNUSED_ARG(gwlen);
        LWIP_UNUSED_ARG(oif);
    }

    return dfltdiff;
}
