/*
Copyright (C) 2018 Srivats P.

This file is part of "Ostinato"

This 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 3 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, see <http://www.gnu.org/licenses/>
*/

#include "linuxhostdevice.h"

#ifdef Q_OS_LINUX

#include "../common/qtport.h"

#include <QHostAddress>

#include <netlink/route/link.h>
#include <netlink/route/neighbour.h>

LinuxHostDevice::LinuxHostDevice(QString portName,
        DeviceManager *deviceManager)
    : Device(deviceManager)
{
    ifName_ = portName;

    netSock_ = nl_socket_alloc();
    if (!netSock_) {
        qWarning("Failed to open netlink socket for %s", qPrintable(ifName_));
        return;
    }

    if (nl_connect(netSock_, NETLINK_ROUTE) < 0) {
        qWarning("Failed to connect netlink socket for %s", qPrintable(ifName_));
        return;
    }

    rtnl_link *link;
    if (rtnl_link_get_kernel(netSock_, 0, qPrintable(ifName_), &link) < 0) {
        qWarning("Failed to get rtnet link from kernel for %s", qPrintable(ifName_));
        return;
    }

    ifIndex_ = rtnl_link_get_ifindex(link);
    qDebug("Port %s: ifIndex %d", qPrintable(ifName_), ifIndex_);

    rtnl_link_put(link);
}

void LinuxHostDevice::receivePacket(PacketBuffer* /*pktBuf*/)
{
    // Do Nothing
}

void LinuxHostDevice::clearNeighbors(Device::NeighborSet set)
{
    // No need to do anything - see AbstractPort::resolveDeviceNeighbors()
    // on when this is used
    if (set == kUnresolvedNeighbors)
        return;

    nl_cache *neighCache;
    if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) {
        qWarning("Failed to get neigh cache from kernel");
        return;
    }

    if (!neighCache) {
        qWarning("Neigh cache empty");
        return;
    }

    int count=0, fail=0;
    rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache);
    while (neigh) {
        if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_) 
                && (rtnl_neigh_get_family(neigh) == AF_INET
                    || rtnl_neigh_get_family(neigh) == AF_INET6)
                && !(rtnl_neigh_get_state(neigh) & (NUD_PERMANENT|NUD_NOARP))) {
            count++;
            if (rtnl_neigh_delete(netSock_, neigh, 0) < 0)
                fail++;
        }
        neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh));
    }
    nl_cache_put(neighCache);
    qDebug("Flush ARP/ND table for ifIndex %u: %d/%d deleted",
             ifIndex_, count - fail, count);
}

void LinuxHostDevice::getNeighbors(OstEmul::DeviceNeighborList *neighbors)
{
    nl_cache *neighCache;
    if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) {
        qWarning("Failed to get neigh cache from kernel");
        return;
    }

    if (!neighCache) {
        qWarning("Neigh cache empty");
        return;
    }

    rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache);
    while (neigh) {
        if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_)
            && !(rtnl_neigh_get_state(neigh) & NUD_NOARP)) {
            if (rtnl_neigh_get_family(neigh) == AF_INET) {
                OstEmul::ArpEntry *arp = neighbors->add_arp();
                arp->set_ip4(qFromBigEndian<quint32>(
                            nl_addr_get_binary_addr(rtnl_neigh_get_dst(neigh))));
                nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh);
                arp->set_mac(lladdr ? 
                                qFromBigEndian<quint64>(
                                    nl_addr_get_binary_addr(lladdr)) >> 16 :
                                0);
            }
            else if (rtnl_neigh_get_family(neigh) == AF_INET6) {
                OstEmul::NdpEntry *ndp = neighbors->add_ndp();
                ndp->mutable_ip6()->set_hi(qFromBigEndian<quint64>(
                                nl_addr_get_binary_addr(
                                                    rtnl_neigh_get_dst(neigh))));
                ndp->mutable_ip6()->set_lo(qFromBigEndian<quint64>((const uchar*)
                                nl_addr_get_binary_addr(
                                                    rtnl_neigh_get_dst(neigh))+8));
                nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh);
                ndp->set_mac(lladdr ? 
                                qFromBigEndian<quint64>(
                                    nl_addr_get_binary_addr(lladdr)) >> 16 :
                                0);
            }
        }
        neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh));
    }
    nl_cache_put(neighCache);
}

quint64 LinuxHostDevice::arpLookup(quint32 ip)
{
    quint64 mac = 0;
    nl_cache *neighCache;
    if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) {
        qWarning("Failed to get neigh cache from kernel");
        return mac;
    }

    if (!neighCache) {
        qWarning("Neigh cache empty");
        return mac;
    }

    quint32 ipBig = qToBigEndian(ip);
    nl_addr *dst = nl_addr_build(AF_INET, &ipBig, sizeof(ipBig));
#if 0
    //
    // libnl 3.2.[15..21] have a bug in rtnl_neigh_get and fail to find entry
    // https://github.com/tgraf/libnl/commit/8571f58f23763d8db7365d02c9b27832ad3d7005
    //
    rtnl_neigh *neigh = rtnl_neigh_get(neighCache, ifIndex_, dst);
    if (neigh) {
        mac = qFromBigEndian<quint64>(
                nl_addr_get_binary_addr(rtnl_neigh_get_lladdr(neigh))) >> 16;
        rtnl_neigh_put(neigh);
    }
#else
    rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache);
    while (neigh) {
        if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_)
                && (rtnl_neigh_get_family(neigh) == AF_INET)
                && !nl_addr_cmp(rtnl_neigh_get_dst(neigh), dst)) {
            nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh);
            if (lladdr)
                mac = qFromBigEndian<quint64>(
                        nl_addr_get_binary_addr(lladdr)) >> 16;
            break;
        }
        neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh));
    }
#endif
    nl_addr_put(dst);
    nl_cache_put(neighCache);

    return mac;
}

quint64 LinuxHostDevice::ndpLookup(UInt128 ip)
{
    quint64 mac = 0;
    nl_cache *neighCache;
    if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) {
        qWarning("Failed to get neigh cache from kernel");
        return mac;
    }

    if (!neighCache) {
        qWarning("Neigh cache empty");
        return mac;
    }

    nl_addr *dst = nl_addr_build(AF_INET6, ip.toArray(), 16);
#if 0
    //
    // libnl 3.2.[15..21] have a bug in rtnl_neigh_get and fail to find entry
    // https://github.com/tgraf/libnl/commit/8571f58f23763d8db7365d02c9b27832ad3d7005
    //
    rtnl_neigh *neigh = rtnl_neigh_get(neighCache, ifIndex_, dst);
    if (neigh) {
        mac = qFromBigEndian<quint64>(
                nl_addr_get_binary_addr(rtnl_neigh_get_lladdr(neigh))) >> 16;
        rtnl_neigh_put(neigh);
    }
#else
    rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache);
    while (neigh) {
        if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_)
                && (rtnl_neigh_get_family(neigh) == AF_INET6)
                && !nl_addr_cmp(rtnl_neigh_get_dst(neigh), dst)) {
            nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh);
            if (lladdr)
                mac = qFromBigEndian<quint64>(
                        nl_addr_get_binary_addr(lladdr)) >> 16;
            break;
        }
        neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh));
    }
#endif
    nl_addr_put(dst);
    nl_cache_put(neighCache);

    return mac;
}

void LinuxHostDevice::sendArpRequest(quint32 tgtIp)
{
    quint32 ipBig = qToBigEndian(tgtIp);
    nl_addr *dst = nl_addr_build(AF_INET, &ipBig, sizeof(ipBig));
    rtnl_neigh *neigh = rtnl_neigh_alloc();
    rtnl_neigh_set_ifindex(neigh, ifIndex_);
    rtnl_neigh_set_state(neigh, NUD_NONE);
    rtnl_neigh_set_dst(neigh, dst);
    rtnl_neigh_set_flags(neigh, NTF_USE); // force kernel to send ARP request
    if (int err = rtnl_neigh_add(netSock_, neigh, NLM_F_CREATE) < 0)
        qWarning("Resolve arp failed for port %s ip %08x: %s",
                qPrintable(ifName_), tgtIp, strerror(err));
    rtnl_neigh_put(neigh);
    nl_addr_put(dst);
}

void LinuxHostDevice::sendNeighborSolicit(UInt128 tgtIp)
{
    nl_addr *dst = nl_addr_build(AF_INET6, tgtIp.toArray(), 16);
    rtnl_neigh *neigh = rtnl_neigh_alloc();
    rtnl_neigh_set_ifindex(neigh, ifIndex_);
    rtnl_neigh_set_state(neigh, NUD_NONE);
    rtnl_neigh_set_dst(neigh, dst);
    rtnl_neigh_set_flags(neigh, NTF_USE); // force kernel to send ARP request
    if (int err = rtnl_neigh_add(netSock_, neigh, NLM_F_CREATE) < 0)
        qWarning("Resolve ndp failed for port %s ip %016llx-%016llx: %s",
                qPrintable(ifName_), tgtIp.hi64(), tgtIp.lo64(), strerror(err));
    rtnl_neigh_put(neigh);
    nl_addr_put(dst);
}

#endif