/*
** ATOP - System & Process Monitor
**
** The program 'atop' offers the possibility to view the activity of
** the system on system-level as well as process-level.
**
** This source-file contains functions to interface with the netatop
** module in the kernel. That module keeps track of network activity
** per process and thread.
** ================================================================
** Author:      Gerlof Langeveld
** E-mail:      gerlof.langeveld@atoptool.nl
** Date:        August/September 2012
**
** This program 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 2, 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.
*/
#define _POSIX_C_SOURCE	200809L
#define _XOPEN_SOURCE
#define _GNU_SOURCE
#define _DEFAULT_SOURCE

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <zlib.h>
#include <sys/mman.h>

#include "atop.h"
#include "photoproc.h"

#include "netatop.h"
#include "netatopd.h"

static int		netsock   = -1;
static int		netexitfd = -1;
static struct naheader	*nahp;
static int		semid     = -1;
static unsigned long	lastseq;

/*
** storage of last exited tasks read from exitfile
** every exitstore struct is registered in hash buckets,
** by its pid or by its begin time
*/
struct exitstore {
	struct exitstore  *next;
	unsigned char      isused;
	struct netpertask  npt;
};

#define NHASH		1024 	// must be power of two!
#define HASHCALC(x)	((x)&(NHASH-1))

static struct exitstore	*esbucket[NHASH];

static struct exitstore *exitall;
static int		 exitnum;
static char		 exithash;

static void	fill_networkcnt(struct tstat *, struct tstat *,
        	                struct exitstore *);

/*
** open a raw socket to the IP layer (root privs required)
*/
void
netatop_ipopen(void)
{
	netsock = socket(PF_INET, SOCK_RAW, IPPROTO_RAW);
}

/*
** check if at this moment the netatop kernel module is loaded and
** the netatopd daemon is active
*/
void
netatop_probe(void)
{
	struct sembuf   	semdecr = {1, -1, SEM_UNDO};
	socklen_t		sl = 0;
	struct stat		exstat;

	/*
 	** check if IP socket is open
	*/
	if (netsock == -1)
		return;

	/*
	** probe if the netatop module is active
	*/
	if ( getsockopt(netsock, SOL_IP, NETATOP_PROBE, NULL, &sl) != 0)
	{
		supportflags &= ~NETATOP;
		supportflags &= ~NETATOPD;
		return;
	}

	// set appropriate support flag
	supportflags |= NETATOP;

	/*
	** check if the netatopd daemon is active to register exited tasks
	** and decrement semaphore to indicate that we want to subscribe
	*/
	if (semid == -1)
	{
		if ( (semid = semget(SEMAKEY, 0, 0))    == -1 ||
	                      semop(semid, &semdecr, 1) == -1   )
		{
			supportflags &= ~NETATOPD;
			return;
		}
	}

	if (semctl(semid, 0, GETVAL, 0) != 1)
	{
		supportflags &= ~NETATOPD;
		return;
	}

	/*
	** check if exitfile still open and not removed by netatopd
	*/
	if (netexitfd != -1)
	{
		if ( fstat(netexitfd, &exstat) == 0 && 
		     exstat.st_nlink > 0              ) // not removed
		{
			supportflags |= NETATOPD;
			return;
		}
		else
		{
			(void) close(netexitfd);

			if (nahp)
				munmap(nahp, sizeof *nahp);

			netexitfd = -1;
			nahp = NULL;
		}
	}

	/*
	** open file with compressed stats of exited tasks
	** and (re)mmap the start record, mainly to obtain current sequence
	*/
	if (netexitfd == -1)
	{
		if ( (netexitfd = open(NETEXITFILE, O_RDONLY, 0)) == -1)
		{
			supportflags &= ~NETATOPD;
               		return;
		}
	}

	if ( (nahp = mmap((void *)0, sizeof *nahp, PROT_READ, MAP_SHARED,
						netexitfd, 0)) == (void *) -1)
	{
        	(void) close(netexitfd);
		netexitfd = -1;
		nahp = NULL;
		supportflags &= ~NETATOPD;
		return;
	}

	/*
	** if this is a new incarnation of the netatopd daemon,
	** position seek pointer on first task that is relevant to us
	** and remember last sequence number to know where to start
	*/
	(void) lseek(netexitfd, 0, SEEK_END);

	lastseq     = nahp->curseq;

	// set appropriate support flag
	supportflags |= NETATOPD;
}

void
netatop_signoff(void)
{
	struct sembuf  	semincr = {1, +1, SEM_UNDO};

	if (netsock == -1 || nahp == NULL)
		return;

	if (supportflags & NETATOPD)
	{
        	regainrootprivs();
		
	        (void) semop(semid, &semincr, 1);

		kill(nahp->mypid, SIGHUP);

       		if (! droprootprivs())
			mcleanstop(42, "failed to drop root privs\n");

		(void) munmap(nahp, sizeof *nahp);
		(void) close(netexitfd);
	}
}

/*
** read network counters for one existing task
** (type 'g' for thread group or type 't' for thread)
*/
void
netatop_gettask(pid_t id, char type, struct tstat *tp)
{
	struct netpertask	npt;
	socklen_t		socklen = sizeof npt;
	int 			cmd = (type == 'g' ?
				    NETATOP_GETCNT_TGID : NETATOP_GETCNT_PID);

	/*
	** if kernel module netatop not active on this system, skip call
	*/
	if (!(supportflags & NETATOP) ) {
		memset(&tp->net, 0, sizeof tp->net);
		return;
	}

	/*
 	** get statistics of this process/thread
	*/
	npt.id	= id;

        regainrootprivs();

	if (getsockopt(netsock, SOL_IP, cmd, &npt, &socklen) != 0) {
		memset(&tp->net, 0, sizeof tp->net);

        	if (! droprootprivs())
			mcleanstop(42, "failed to drop root privs\n");

		if (errno == ENOPROTOOPT || errno == EPERM)
		{
			supportflags &= ~NETATOP;
			supportflags &= ~NETATOPD;
			close(netsock);
			netsock = -1;
		}

		return;
	}

       	if (! droprootprivs())
		mcleanstop(42, "failed to drop root privs\n");

	/*
	** statistics available: fill counters
	*/
	tp->net.tcpsnd = npt.tc.tcpsndpacks;
	tp->net.tcprcv = npt.tc.tcprcvpacks;
	tp->net.tcpssz = npt.tc.tcpsndbytes;
	tp->net.tcprsz = npt.tc.tcprcvbytes;

	tp->net.udpsnd = npt.tc.udpsndpacks;
	tp->net.udprcv = npt.tc.udprcvpacks;
	tp->net.udpssz = npt.tc.udpsndbytes;
	tp->net.udprsz = npt.tc.udprcvbytes;
}

/*
** read all exited processes that have been added to the exitfile
** and store them into memory
*/
unsigned int
netatop_exitstore(void)
{
	socklen_t		socklen = 0, nexitnet, sz, nr=0;
	unsigned long		uncomplen;
	unsigned char		nextsize;
	unsigned char		readbuf[nahp->ntplen+100];
	unsigned char		databuf[nahp->ntplen];
	struct netpertask	*tmp = (struct netpertask *)databuf;
	struct exitstore	*esp;

        regainrootprivs();

	/*
	** force garbage collection:
	**   netatop module builds new list of exited processes that
	**   can be read by netatopd and written to exitfile
	*/
	if (getsockopt(netsock, SOL_IP, NETATOP_FORCE_GC, NULL, &socklen)!=0) {
        	if (! droprootprivs())
			mcleanstop(42, "failed to drop root privs\n");

		if (errno == ENOPROTOOPT || errno == EPERM)
		{
			supportflags &= ~NETATOP;
			supportflags &= ~NETATOPD;
			close(netsock);
			netsock = -1;
		}

		return 0;
	}

	/*
 	** wait until list of exited processes is read by netatopd
	** and available to be read by atop
	*/
	if (getsockopt(netsock, SOL_IP, NETATOP_EMPTY_EXIT, 0, &socklen) !=0) {
        	if (! droprootprivs())
			mcleanstop(42, "failed to drop root privs\n");

		if (errno == ENOPROTOOPT || errno == EPERM)
		{
			supportflags &= ~NETATOP;
			supportflags &= ~NETATOPD;
			close(netsock);
			netsock = -1;
		}

		return 0;
	}

       	if (! droprootprivs())
		mcleanstop(42, "failed to drop root privs\n");

	/*
	** verify how many exited processes are available to be read
	** from the exitfile
	*/
	nexitnet = nahp->curseq - lastseq;
	lastseq  = nahp->curseq;

	if (nexitnet == 0)
		return 0;

	/*
	** allocate storage for all exited processes
	*/
	exitall = malloc(nexitnet * sizeof(struct exitstore));

	ptrverify(exitall, "Malloc failed for %d exited netprocs\n", nexitnet);

	memset(exitall, 0, nexitnet * sizeof(struct exitstore));

	esp = exitall;

	/*
	** read next byte from exitfile that specifies the length
	** of the next record
	*/
	if ( read(netexitfd, &nextsize, 1) != 1) 
		return 0;

	/*
	** read the next record and (if possible) the byte specifying
	** the size of the next record
	*/
	while ( (sz = read(netexitfd, readbuf, nextsize+1)) >= nextsize)
	{
		/*
		** decompress record and store it
		*/
        	uncomplen = nahp->ntplen;

		if (nahp->ntplen <= sizeof(struct netpertask))
		{
			(void) uncompress((Byte *)&(esp->npt), &uncomplen,
							readbuf, nextsize);
		}
		else
		{
			(void) uncompress((Byte *)databuf, &uncomplen,
							readbuf, nextsize);
			esp->npt = *tmp;
		}

		esp++;
		nr++;

		/*
		** check if we have read all records
		*/
		if (nr == nexitnet)
		{
			/*
			** if we have read one byte too many:
			** reposition seek pointer 
			*/
			if (sz > nextsize)
				(void) lseek(netexitfd, -1, SEEK_CUR);

			break;
		}

		/*
		** prepare reading next record
		*/
		if (sz > nextsize)
			nextsize = readbuf[nextsize];
		else
			break;	// unexpected: more requested than available
	}

	exitnum = nr;

	return nr;
}

/*
** remove all stored exited processes from the hash bucket list
*/
void
netatop_exiterase(void)
{
	free(exitall);
	memset(esbucket, 0, sizeof esbucket);
	exitnum = 0;
}

/*
** add all stored tasks to a hash bucket, either
** by pid (argument 'p') or by begin time (argument 'b')
*/
void
netatop_exithash(char hashtype)
{
	int			i, h;
	struct exitstore	*esp;

	for (i=0, esp=exitall; i < exitnum; i++, esp++)
	{
		if (hashtype == 'p')
			h = HASHCALC(esp->npt.id);
		else
			h = HASHCALC(esp->npt.btime);

		esp->next   = esbucket[h];
		esbucket[h] = esp;
	}

	exithash = hashtype;
}

/*
** search for relevant exited network task and
** update counters in tstat struct
*/
void
netatop_exitfind(unsigned long key, struct tstat *dev, struct tstat *pre)
{
	int 			h = HASHCALC(key);
	struct exitstore	*esp;

	/*
	** if bucket empty, forget about it
	*/
	if ( (esp = esbucket[h]) == NULL)
		return;

	/*
	** search thru hash bucket list
	*/
	for (; esp; esp=esp->next)
	{
		switch (exithash)
		{
		   case 'p':		// search by PID
			if (key != esp->npt.id)
				continue;

			/*
			** correct PID found
			*/
			fill_networkcnt(dev, pre, esp);
			break;

		   case 'b':		// search by begin time
			if (esp->isused)
				continue;

			if (key != esp->npt.btime)
				continue;

			/*
			** btime is okay; additional checks required
			*/
			if ( strcmp(esp->npt.command, pre->gen.name) != 0)
				continue;

			if (esp->npt.tc.tcpsndpacks < pre->net.tcpsnd 	||
			    esp->npt.tc.tcpsndbytes < pre->net.tcpssz 	||
			    esp->npt.tc.tcprcvpacks < pre->net.tcprcv 	||
			    esp->npt.tc.tcprcvbytes < pre->net.tcprsz 	||
			    esp->npt.tc.udpsndpacks < pre->net.udpsnd 	||
			    esp->npt.tc.udpsndbytes < pre->net.udpssz 	||
			    esp->npt.tc.udprcvpacks < pre->net.udprcv 	||
			    esp->npt.tc.udprcvbytes < pre->net.udprsz 	  )
				continue;

			esp->isused = 1;
 
			fill_networkcnt(dev, pre, esp);
			break;
		}
	}
}

static void
fill_networkcnt(struct tstat *dev, struct tstat *pre, struct exitstore *esp)
{
	dev->net.tcpsnd	= esp->npt.tc.tcpsndpacks - pre->net.tcpsnd;
	dev->net.tcpssz	= esp->npt.tc.tcpsndbytes - pre->net.tcpssz;
	dev->net.tcprcv	= esp->npt.tc.tcprcvpacks - pre->net.tcprcv;
	dev->net.tcprsz	= esp->npt.tc.tcprcvbytes - pre->net.tcprsz;

	dev->net.udpsnd	= esp->npt.tc.udpsndpacks - pre->net.udpsnd;
	dev->net.udpssz	= esp->npt.tc.udpsndbytes - pre->net.udpssz;
	dev->net.udprcv	= esp->npt.tc.udprcvpacks - pre->net.udprcv;
	dev->net.udprsz	= esp->npt.tc.udprcvbytes - pre->net.udprsz;
}
