/* iface-socket.cc
****************************************************************************************************************
****************************************************************************************************************

    Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2020, 2021, 2026 Askar
    Almatov

    This file is part of Trafficmeter.

    Trafficmeter 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.

    Trafficmeter 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 Trafficmeter. If not, see
    <http://www.gnu.org/licenses/>.
*/

#include <cassert>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <libiclass/time>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <linux/if_packet.h>

#include "iface.h"
#include "erspan.h"

using namespace trafficmeter;

using std::hex;
using std::ifstream;
using std::invalid_argument;
using std::runtime_error;
using std::string;
using std::stringstream;
using std::to_string;

constexpr const unsigned    BLOCK_SIZE_         = 1U << 20;             // 1 MiB
constexpr const unsigned    RX_RING_SIZE_       = 128 * BLOCK_SIZE_;    // 128 MiB
constexpr const unsigned    RECORD_ALIGNMENT_   = 8;                    // kernel source V3_ALIGNMENT value

/**************************************************************************************************************/
static bool
bindInterface_( int fd, const string& name, bool isPromisc )
{
    struct sockaddr_ll  sll;
    int                 error;
    socklen_t           errorLength = sizeof( error );
    unsigned            interfaceNo = if_nametoindex( name.c_str() );

    memset( &sll, 0, sizeof(sll) );
    sll.sll_family = AF_PACKET;
    sll.sll_ifindex = interfaceNo;
    sll.sll_protocol = htons( ETH_P_ALL );

    if
    (
        bind( fd, reinterpret_cast<struct sockaddr*>(&sll), sizeof(sll) ) != 0 ||
        getsockopt( fd, SOL_SOCKET, SO_ERROR, &error, &errorLength ) != 0 ||
        error != 0
    )
    {
        return false;
    }

    if ( isPromisc )
    {
        struct packet_mreq  mr;

        memset( &mr, 0, sizeof(mr) );
        mr.mr_ifindex = interfaceNo;
        mr.mr_type = PACKET_MR_PROMISC;
        assert( setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr)) == 0 );
    }

    return true;
}

/**************************************************************************************************************/
static string
getLinkType_( const string& name )
{
    string      linkType;
    ifstream    typeInput( "/sys/class/net/" + name + "/type" );
    int         type;

    typeInput >> type;

    if ( typeInput.good() )
    {
        switch ( type )
        {
            case ARPHRD_LOOPBACK:

                linkType = "Loopback";
                break;

            case ARPHRD_ETHER:

                stringstream    vlanConfig;

                linkType = "Ethernet";
                vlanConfig << ifstream( "/proc/net/vlan/config" ).rdbuf();

                if ( vlanConfig.str().find(name + '.') != string::npos )
                {
                    linkType += " 802.1Q";
                }
        }
    }

    return std::move( linkType );
}

/**************************************************************************************************************/
static bool
isPromisc_( const string& name )
{
    ifstream    flagsInput( "/sys/class/net/" + name + "/flags" );
    int         flags;

    flagsInput >> hex >> flags;
    return flagsInput.good()? flags & IFF_PROMISC : false;
}

/**************************************************************************************************************/
static int
getMtu_( int fd, const string& name )
{
    struct ifreq  ifr;

    memset( &ifr, 0, sizeof(ifr) );
    strncpy( ifr.ifr_name, name.c_str(), sizeof(ifr.ifr_name) );
    ifr.ifr_addr.sa_family = AF_INET;
    assert( ioctl(fd, SIOCGIFMTU, &ifr) != -1 );
    return ifr.ifr_mtu;
}

/**************************************************************************************************************/
Iface::Iface( const string& name, bool isPromisc ) :
    Source( name ),
    rxRing_( nullptr ),
    rxRingSize_( RX_RING_SIZE_ ),
    block_( nullptr ),
    blockSize_( BLOCK_SIZE_ ),
    record_( nullptr ),
    nRecords_( 0 )
{
    if ( name_.size() > IFNAMSIZ )
    {
        throw invalid_argument( "interface name is too long" );
    }

    if ( if_nametoindex(name.c_str()) == 0 )
    {
        throw runtime_error( "interface does not exist" );
    }

    fd_ = socket( AF_PACKET, SOCK_RAW, htons(ETH_P_ALL) );

    if ( fd_ < 0 )
    {
        throw runtime_error( "cannot create socket" );
    }

    if ( !bindInterface_(fd_, name_, isPromisc) )
    {
        ::close( fd_ );
        throw runtime_error( "interface is down or invalid" );
    }

    int  version( TPACKET_V3 );

    if ( setsockopt(fd_, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) )
    {
        ::close( fd_ );
        throw runtime_error( "the kernel does not support TPACKET_V3" );
    }

    struct tpacket_req3  request;

    memset( &request, 0, sizeof(request) );
    request.tp_block_size = blockSize_;
    request.tp_frame_size = blockSize_;
    request.tp_block_nr = rxRingSize_ / blockSize_;
    request.tp_frame_nr = rxRingSize_ / blockSize_;
    request.tp_retire_blk_tov = 64;

    if ( setsockopt(fd_, SOL_PACKET, PACKET_RX_RING, &request, sizeof(request)) )
    {
        ::close( fd_ );

        if ( errno == ENOMEM )
        {
            throw runtime_error( "requested RX ring size is too big" );
        }

        throw runtime_error( "cannot create RX ring on the socket" );
    }

    void*  mapping( mmap(0, rxRingSize_, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_LOCKED, fd_, 0) );

    if ( mapping == MAP_FAILED )
    {
        ::close( fd_ );
        throw runtime_error( "cannot map RX ring into memory" );
    }

    struct tpacket_stats_v3     statistics;
    socklen_t                   statisticsLength = sizeof( statistics );

    getsockopt( fd_, SOL_PACKET, PACKET_STATISTICS, &statistics, &statisticsLength );
    rxRing_ = block_ = static_cast<uint8_t*>( mapping );
    linkType_ = getLinkType_( name_ );

    info_ =
    (
        linkType_ + ( linkType_.empty()? "" : " " ) +
        ( isPromisc_( name_ )? "promisc " : "" ) +
        ( shouldCheckErspan_? "erspan " : "" ) +
        "mtu=" + to_string( getMtu_(fd_, name_) ) + " " +
        "rxring=" + to_string( rxRingSize_ >> 20 ) + "MiB " +
        __FILE__
    );
}

/**************************************************************************************************************/
Iface::~Iface()
{
    if ( rxRing_ != nullptr )
    {
        munmap( rxRing_, rxRingSize_ );
    }

    if ( fd_ >= 0 )
    {
        ::close( fd_ );
    }
}

/**************************************************************************************************************/
void
Iface::open()
{
}

/**************************************************************************************************************/
void
Iface::close()
{
}

/**************************************************************************************************************/
bool
Iface::checkForRecords()
{
    if ( nRecords_ > 0 )
    {
        return true;
    }

    auto*   bd = reinterpret_cast<struct tpacket_block_desc*>( block_ );

    if ( record_ != nullptr && bd->hdr.bh1.block_status != TP_STATUS_KERNEL)
    {
        bd->hdr.bh1.block_status = TP_STATUS_KERNEL;
        block_ += blockSize_;

        if ( block_ >= rxRing_ + rxRingSize_ )
        {
            block_ = rxRing_;
        }

        bd = reinterpret_cast<struct tpacket_block_desc*>( block_ );
    }

    if ( record_ == nullptr || bd->hdr.bh1.block_status == TP_STATUS_KERNEL )
    {
        struct pollfd   fds = { .fd = fd_, .events = POLLIN };
        int             nRevents = poll( &fds, 1, SOURCE_TIMEOUT );

        if ( nRevents <= 0 || (fds.revents & (POLLHUP|POLLRDHUP|POLLERR|POLLNVAL)) )
        {
            return false;
        }

        struct tpacket_stats_v3     statistics;
        socklen_t                   statisticsLength = sizeof( statistics );

        getsockopt( fd_, SOL_PACKET, PACKET_STATISTICS, &statistics, &statisticsLength );
        nDropped_a_ += statistics.tp_drops;
    }

    record_ = block_ + bd->hdr.bh1.offset_to_first_pkt;
    nRecords_ = bd->hdr.bh1.num_pkts;
    return true;
}

/**************************************************************************************************************/
bool
Iface::copyRecords( uint8_t** destination, const uint8_t* destinationEnd )
{
    unsigned    nBytesToCopy = 0;
    unsigned    nRecordsOld = nRecords_;
    uint8_t*    record = record_;
    bool        hasRecordsLeft = false;

    while ( nRecords_ > 0 )
    {
        const auto*     header = reinterpret_cast<struct tpacket3_hdr*>( record );
        unsigned        recordSize = align( header->tp_mac + header->tp_snaplen, RECORD_ALIGNMENT_ );

        if ( *destination + nBytesToCopy + recordSize > destinationEnd )
        {
            hasRecordsLeft = true;
            break;
        }

        nBytesToCopy += recordSize;
        record += recordSize;
        --nRecords_;
    }

    memcpy( *destination, record_, nBytesToCopy );
    nReceived_a_ += nRecordsOld - nRecords_;
    record_ = record;
    *destination += nBytesToCopy;
    return hasRecordsLeft;
}

/**************************************************************************************************************/
Source::Packet
Iface::getPacket()
{
    const auto*     header = reinterpret_cast<struct tpacket3_hdr*>( record_ );
    Timestamp       timestamp( header->tp_sec, header->tp_nsec );
    uint8_t*        data2 = record_ + header->tp_mac;
    unsigned        nBytes2 = header->tp_len;

    if ( header->tp_status & TP_STATUS_VLAN_VALID )
    {
        data2 -= 4;  // 4 bytes space after header for vlan tag reconstruction (see kernel source)
        nBytes2 += 4;
        memmove( data2, data2 + 4, 12 );
        *reinterpret_cast<uint32_t*>( data2 + 12 ) = htonl( 0x81000000U | header->hv1.tp_vlan_tci );
    }

    if ( shouldCheckErspan_ )
    {
        Erspan  erspan( data2, nBytes2 );

        data2 = const_cast<uint8_t*>( erspan.data2() );
        nBytes2 = erspan.nBytes2();
    }

    ++nReceived_a_;
    --nRecords_;
    record_ += header->tp_next_offset;
    return { timestamp, data2, nBytes2 };
}

/**************************************************************************************************************/
Source::Packet
Iface::getPacketFromCopy( uint8_t** record ) const
{
    const auto*     header = reinterpret_cast<struct tpacket3_hdr*>( *record );
    unsigned        recordSize = align( header->tp_mac + header->tp_snaplen, RECORD_ALIGNMENT_ );
    Timestamp       timestamp( header->tp_sec, header->tp_nsec );
    uint8_t*        data2 = *record + header->tp_mac;
    unsigned        nBytes2 = header->tp_len;

    if ( header->tp_status & TP_STATUS_VLAN_VALID )
    {
        data2 -= 4;  // 4 bytes space after header for vlan tag reconstruction (see kernel source)
        nBytes2 += 4;
        memmove( data2, data2 + 4, 12 );
        *reinterpret_cast<uint32_t*>( data2 + 12 ) = htonl( 0x81000000U | header->hv1.tp_vlan_tci );
    }

    if ( shouldCheckErspan_ )
    {
        Erspan  erspan( data2, nBytes2 );

        data2 = const_cast<uint8_t*>( erspan.data2() );
        nBytes2 = erspan.nBytes2();
    }

    *record += recordSize;
    return { timestamp, data2, nBytes2 };
}

/**************************************************************************************************************/
bool
Iface::isEthernet() const
{
    return linkType_.find( "Ethernet" ) != string::npos;
}
