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

#include "winutils.h"

#include "proxy.h"

#include "lwip/opt.h"
#include "lwip/sys.h"
#include "lwip/stats.h"
#include "lwip/timers.h"

#include "lwip/inet_chksum.h"
#include "lwip/icmp6.h"
#include "lwip/nd6.h"

#include "lwip/raw.h"

#include <string.h>


static void proxy_rtadvd_timer(void *);
static void proxy_rtadvd_send_multicast(struct netif *);
static void proxy_rtadvd_fill_payload(struct netif *, int);

static u8_t rtadvd_recv(void *, struct raw_pcb *, struct pbuf *, ip6_addr_t *);


/* ff02::1 - link-local all nodes multicast address */
static ip6_addr_t allnodes_linklocal = {
    { PP_HTONL(0xff020000UL), 0, 0, PP_HTONL(0x00000001UL) }
};


/*
 * Unsolicited Router Advertisement payload.
 *
 * NB: Since ICMP checksum covers pseudo-header with destination
 * address (link-local allnodes multicast in this case) this payload
 * cannot be used for solicited replies to unicast addresses.
 */
static unsigned int unsolicited_ra_payload_length;
static u8_t unsolicited_ra_payload[
      sizeof(struct ra_header)
      /* reserves enough space for NETIF_MAX_HWADDR_LEN */
    + sizeof(struct lladdr_option)
      /* we only announce one prefix */
    + sizeof(struct prefix_option) * 1
];


static int ndefaults = 0;

static struct raw_pcb *rtadvd_pcb;


void
proxy_rtadvd_start(struct netif *proxy_netif)
{
#if 0 /* XXX */
    ndefaults = rtmon_get_defaults();
#else
    ndefaults = g_proxy_options->ipv6_defroute;
#endif
    if (ndefaults < 0) {
        DPRINTF0(("rtadvd: failed to read IPv6 routing table, aborting\n"));
        return;
    }

    proxy_rtadvd_fill_payload(proxy_netif, ndefaults > 0);

    rtadvd_pcb = raw_new_ip6(IP6_NEXTH_ICMP6);
    if (rtadvd_pcb == NULL) {
        DPRINTF0(("rtadvd: failed to allocate pcb, aborting\n"));
        return;
    }

    /*
     * We cannot use raw_bind_ip6() since raw_input() doesn't grok
     * multicasts.  We are going to use ip6_output_if() directly.
     */
    raw_recv_ip6(rtadvd_pcb, rtadvd_recv, proxy_netif);

    sys_timeout(3 * 1000, proxy_rtadvd_timer, proxy_netif);
}


static int quick_ras = 2;


/**
 * lwIP thread callback invoked when we start/stop advertising default
 * route.
 */
void
proxy_rtadvd_do_quick(void *arg)
{
    struct netif *proxy_netif = (struct netif *)arg;

    quick_ras = 2;
    sys_untimeout(proxy_rtadvd_timer, proxy_netif);
    proxy_rtadvd_timer(proxy_netif); /* sends and re-arms */
}


static void
proxy_rtadvd_timer(void *arg)
{
    struct netif *proxy_netif = (struct netif *)arg;
    int newdefs;
    u32_t delay;

#if 0 /* XXX */
    newdefs = rtmon_get_defaults();
#else
    newdefs = g_proxy_options->ipv6_defroute;
#endif
    if (newdefs != ndefaults && newdefs != -1) {
        ndefaults = newdefs;
        proxy_rtadvd_fill_payload(proxy_netif, ndefaults > 0);
    }

    proxy_rtadvd_send_multicast(proxy_netif);

    if (quick_ras > 0) {
        --quick_ras;
        delay = 16 * 1000;
    }
    else {
        delay = 600 * 1000;
    }

    sys_timeout(delay, proxy_rtadvd_timer, proxy_netif);
}


/*
 * This should be folded into icmp6/nd6 input, but I don't want to
 * solve this in general, making it configurable, etc.
 *
 * Cf. RFC 4861:
 *   6.1.1. Validation of Router Solicitation Messages
 */
static u8_t
rtadvd_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip6_addr_t *addr)
{
    enum raw_recv_status { RAW_RECV_CONTINUE = 0, RAW_RECV_CONSUMED = 1 };

    struct netif *proxy_netif = (struct netif *)arg;
    struct ip6_hdr *ip6_hdr;
    struct icmp6_hdr *icmp6_hdr;
    struct lladdr_option *lladdr_opt;
    void *option;
    u8_t opttype, optlen8;

    LWIP_UNUSED_ARG(pcb);
    LWIP_UNUSED_ARG(addr);

    /* save a pointer to IP6 header and skip to ICMP6 payload */
    ip6_hdr = (struct ip6_hdr *)p->payload;
    pbuf_header(p, -ip_current_header_tot_len());

    if (p->len < sizeof(struct icmp6_hdr)) {
        ICMP6_STATS_INC(icmp6.lenerr);
        goto drop;
    }

    if (ip6_chksum_pseudo(p, IP6_NEXTH_ICMP6, p->tot_len,
                          ip6_current_src_addr(),
                          ip6_current_dest_addr()) != 0)
    {
        ICMP6_STATS_INC(icmp6.chkerr);
        goto drop;
    }

    icmp6_hdr = (struct icmp6_hdr *)p->payload;
    if (icmp6_hdr->type != ICMP6_TYPE_RS) {
        pbuf_header(p, ip_current_header_tot_len()); /* restore payload ptr */
        return RAW_RECV_CONTINUE; /* not interested */
    }

    /* only now that we know it's ICMP6_TYPE_RS we can check IP6 hop limit */
    if (IP6H_HOPLIM(ip6_hdr) != 255) {
        ICMP6_STATS_INC(icmp6.proterr);
        goto drop;
    }

    /* future, backward-incompatible changes may use different Code values. */
    if (icmp6_hdr->code != 0) {
        ICMP6_STATS_INC(icmp6.proterr);
        goto drop;
    }

    /* skip past rs_header, nothing interesting in it */
    if (p->len < sizeof(struct rs_header)) {
        ICMP6_STATS_INC(icmp6.lenerr);
        goto drop;
    }
    pbuf_header(p, -(s16_t)sizeof(struct rs_header));

    lladdr_opt = NULL;
    while (p->len > 0) {
        int optlen;

        if (p->len < 8) {
            ICMP6_STATS_INC(icmp6.lenerr);
            goto drop;
        }

        option = p->payload;
        opttype = ((u8_t *)option)[0];
        optlen8 = ((u8_t *)option)[1]; /* in units of 8 octets */

        if (optlen8 == 0) {
            ICMP6_STATS_INC(icmp6.proterr);
            goto drop;
        }

        optlen = (unsigned int)optlen8 << 3;
        if (p->len < optlen) {
            ICMP6_STATS_INC(icmp6.lenerr);
            goto drop;
        }

        if (opttype == ND6_OPTION_TYPE_SOURCE_LLADDR) {
            if (lladdr_opt != NULL) { /* duplicate */
                ICMP6_STATS_INC(icmp6.proterr);
                goto drop;
            }
            lladdr_opt = (struct lladdr_option *)option;
        }

        pbuf_header(p, -optlen);
    }

    if (ip6_addr_isany(ip6_current_src_addr())) {
        if (lladdr_opt != NULL) {
            ICMP6_STATS_INC(icmp6.proterr);
            goto drop;
        }

        /* reply with multicast RA */
    }
    else {
        /*
         * XXX: Router is supposed to update its Neighbor Cache (6.2.6),
         * but it's hidden inside nd6.c.
         */

        /* may reply with either unicast or multicast RA */
    }
    /* we just always reply with multicast RA */

    pbuf_free(p);               /* NB: this invalidates lladdr_option */

    sys_untimeout(proxy_rtadvd_timer, proxy_netif);
    proxy_rtadvd_timer(proxy_netif); /* sends and re-arms */

    return RAW_RECV_CONSUMED;

  drop:
    pbuf_free(p);
    ICMP6_STATS_INC(icmp6.drop);
    return RAW_RECV_CONSUMED;
}


static void
proxy_rtadvd_send_multicast(struct netif *proxy_netif)
{
    struct pbuf *ph, *pp;
    err_t error;

    ph = pbuf_alloc(PBUF_IP, 0, PBUF_RAM);
    if (ph == NULL) {
        DPRINTF0(("%s: failed to allocate RA header pbuf\n", __func__));
        return;
    }

    pp = pbuf_alloc(PBUF_RAW, unsolicited_ra_payload_length, PBUF_ROM);
    if (pp == NULL) {
        DPRINTF0(("%s: failed to allocate RA payload pbuf\n", __func__));
        pbuf_free(ph);
        return;
    }
    pp->payload = unsolicited_ra_payload;
    pbuf_chain(ph, pp);

    error = ip6_output_if(ph,
                          netif_ip6_addr(proxy_netif, 0), /* src: link-local */
                          &allnodes_linklocal,            /* dst */
                          255,                            /* hop limit */
                          0,                              /* traffic class */
                          IP6_NEXTH_ICMP6,
                          proxy_netif);
    if (error != ERR_OK) {
        DPRINTF0(("%s: failed to send RA (err=%d)\n", __func__, error));
    }

    pbuf_free(pp);
    pbuf_free(ph);
}


/*
 * XXX: TODO: Only ra_header::router_lifetime (and hence
 * ra_header::chksum) need to be changed, so we can precompute it once
 * and then only update these two fields.
 */
static void
proxy_rtadvd_fill_payload(struct netif *proxy_netif, int is_default)
{
    struct pbuf *p;
    struct ra_header *ra_hdr;
    struct lladdr_option *lladdr_opt;
    struct prefix_option *pfx_opt;
    unsigned int lladdr_optlen;

    LWIP_ASSERT("netif hwaddr too long",
                proxy_netif->hwaddr_len <= NETIF_MAX_HWADDR_LEN);

    /* type + length + ll addr + round up to 8 octets */
    lladdr_optlen = (2 + proxy_netif->hwaddr_len + 7) & ~0x7;

    /* actual payload length */
    unsolicited_ra_payload_length =
          sizeof(struct ra_header)
        + lladdr_optlen
        + sizeof(struct prefix_option) * 1;

    /* Set fields. */
    ra_hdr = (struct ra_header *)unsolicited_ra_payload;
    lladdr_opt = (struct lladdr_option *)((u8_t *)ra_hdr + sizeof(struct ra_header));
    pfx_opt = (struct prefix_option *)((u8_t *)lladdr_opt + lladdr_optlen);

    memset(unsolicited_ra_payload, 0, sizeof(unsolicited_ra_payload));

    ra_hdr->type = ICMP6_TYPE_RA;

#if 0
    /*
     * "M" flag.  Tell guests to use stateful DHCP6.  Disabled here
     * since we don't provide stateful server.
     */
    ra_hdr->flags |= ND6_RA_FLAG_MANAGED_ADDR_CONFIG;
#endif
    /*
     * XXX: TODO: Disable "O" flag for now to match disabled stateless
     * server.  We don't yet get IPv6 nameserver addresses from
     * HostDnsService, so we have nothing to say, don't tell guests to
     * come asking.
     */
#if 0
    /*
     * "O" flag.  Tell guests to use DHCP6 for DNS and the like.  This
     * is served by simple stateless server (RFC 3736).
     *
     * XXX: "STATEFUL" in the flag name was probably a bug in RFC2461.
     * It's present in the text, but not in the router configuration
     * variable name.  It's dropped in the text in RFC4861.
     */
    ra_hdr->flags |= ND6_RA_FLAG_OTHER_STATEFUL_CONFIG;
#endif

    if (is_default) {
        ra_hdr->router_lifetime = PP_HTONS(1200); /* seconds */
    }
    else {
        ra_hdr->router_lifetime = 0;
    }

    lladdr_opt->type = ND6_OPTION_TYPE_SOURCE_LLADDR;
    lladdr_opt->length = lladdr_optlen >> 3; /* in units of 8 octets */
    memcpy(lladdr_opt->addr, proxy_netif->hwaddr, proxy_netif->hwaddr_len);

    pfx_opt->type = ND6_OPTION_TYPE_PREFIX_INFO;
    pfx_opt->length = 4;
    pfx_opt->prefix_length = 64;
    pfx_opt->flags = ND6_PREFIX_FLAG_ON_LINK
        | ND6_PREFIX_FLAG_AUTONOMOUS;
    pfx_opt->valid_lifetime = ~0U; /* infinite */
    pfx_opt->preferred_lifetime = ~0U; /* infinite */
    pfx_opt->prefix.addr[0] = netif_ip6_addr(proxy_netif, 1)->addr[0];
    pfx_opt->prefix.addr[1] = netif_ip6_addr(proxy_netif, 1)->addr[1];


    /* we need a temp pbuf to calculate the checksum */
    p = pbuf_alloc(PBUF_IP, unsolicited_ra_payload_length, PBUF_ROM);
    if (p == NULL) {
        DPRINTF0(("rtadvd: failed to allocate RA pbuf\n"));
        return;
    }
    p->payload = unsolicited_ra_payload;

    ra_hdr->chksum = ip6_chksum_pseudo(p, IP6_NEXTH_ICMP6, p->len,
                                       /* src addr: netif's link-local */
                                       netif_ip6_addr(proxy_netif, 0),
                                       /* dst addr */
                                       &allnodes_linklocal);
    pbuf_free(p);
}
