/* 
 * Network accounting
 * capture.c - capture raw packets
 * (C) 1994 Ulrich Callmeier
 */

#include <sys/time.h>
#include <sys/wait.h>
#include "netacct.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <malloc.h>
#include <strings.h>
#include <signal.h>
#include <fcntl.h>
#include <utmp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netinet/protocols.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

char *rcs_revision_capture_c = "$Revision: 1.10 $";

void handle_ip(unsigned char buf[], char *devname, char *user);
void register_packet(unsigned long int src,unsigned long int dst, unsigned char proto, unsigned short srcport, unsigned short dstport, int size, char *devname, char *user);

volatile int running;

void init_capture()
/*
 * 1) Open our capture socket
 * 2) Set all the promisc devices to promiscous mode 
 */
{
    struct ifreq ifr;
    struct promisc_device *p;

    if ((capture_sd = socket (AF_INET, SOCK_PACKET, htons (ETH_P_ALL))) < 0)
	{
	    syslog(LOG_ERR, "can't get socket: %m\n");
	    daemon_stop(0);
	}

    p = cfg -> promisc;
    
    while(p!=NULL)
	{
	    strcpy (p -> oldifr.ifr_name, p -> name);
	    
	    if (ioctl (capture_sd, SIOCGIFFLAGS, &(p -> oldifr)) < 0)
		{
		    syslog(LOG_ERR, "can't get flags: %m\n");
		    daemon_stop(0);
		}
	    
	    p -> reset = 1;
	    
	    ifr = p -> oldifr;
	    ifr.ifr_flags |= IFF_PROMISC;
	    
	    if (ioctl (capture_sd, SIOCSIFFLAGS, &ifr) < 0)
		{
		    syslog(LOG_ERR, "can't set flags: %m\n");
		    daemon_stop(0);
		}

	    syslog(LOG_DEBUG, "%s set to promiscous mode\n", p -> name);
	    
	    p = p -> next;
	}
}

void exit_capture(void)
{
    struct promisc_device *p;

    /* do we have to check (capture_sd >= 0) ? */

    p = cfg -> promisc;
    
    while(p != NULL)
	{
	    if (ioctl (capture_sd, SIOCSIFFLAGS, &(p -> oldifr)) < 0)
		{
		    syslog(LOG_ERR, "can't reset flags: %m\n");
		}
	    
	    p = p -> next;
	}
    
    close (capture_sd);
}


static struct statistics *packets;

static struct ipdata *plist; /* data being collected */
static struct ipdata *olist; /* data being written */
unsigned long int plistsize;
unsigned long int olistsize;
static volatile sig_atomic_t lck;
static volatile sig_atomic_t writing;
static volatile sig_atomic_t dumping;
volatile pid_t writepid;
volatile pid_t dumppid;

volatile int may_write;

int err_delay, max_err_delay;

volatile time_t now;

struct ipnetwork *ignorenet;

/* statistics */
unsigned int list_compares, list_lookups;

void reopen_socket(void)
{
    int save1, save2;
    
    /* critical section */
    save1 = may_write;
    may_write = 0;
    save2 = lck;
    lck = 1;
    /* end */
    
    exit_capture();
    init_capture();

    lck = save2;
    may_write = save1;
}

int netignore(unsigned long int addr)
{
    struct ipnetwork *tmp;
    tmp = ignorenet;

    while(tmp!=NULL)
	{
	    if((addr & tmp -> netmask) == tmp -> netnumber)
		{
		    return 1;
		}
	    tmp = tmp -> next;
	}
    return 0;
}

struct dynadat *dynadat = NULL;

char *check_user(char *devname)
/*
 * Find username corresponding to devname
 */
{
    struct dev2line *d2l;
    char *line;
    struct dynadat *dd;
    int found;

    d2l = dev2line;
    line = NULL;

    while(d2l!=NULL)
	{
	    if(strcmp(d2l -> netinterface, devname)==0)
		{
		    line = d2l -> line;
		    break;
		}
	    
	    d2l = d2l -> next;
	}
    
    if(line == NULL)
	return NULL;
    
    dd = dynadat;

    found = 0;
    while(dd != NULL)
	{
	    if(strcmp(dd -> netinterface, devname) == 0)
		{
		    found = 1;
		    break;
		}
	    dd = dd -> next;
	}
	    
    if(!found)
	{
	    /* We don't have an entry for this device yet. Add one */

	    dd = malloc(sizeof(struct dynadat));
	    
	    dd -> netinterface = strdup(devname);
	    dd -> last_stat = 0; /* force reading of utmp */
	    dd -> mtime = 0;
	    dd -> user = NULL;
	    dd -> next = dynadat;
	    dynadat = dd;
	}

    /* dd now points to the right dynadat entry */
    /* maybe this is out of date, so we check */
	    
    if ((now - dd->last_stat) > FORCE_STAT_TIME )
	{
	    struct stat statbuf;
	    
	    /* it could be invalid, lets stat utmp */
	    
	    if(stat(_PATH_UTMP, &statbuf)==0)
		{
		    if(debug_level > 1)
			syslog(LOG_DEBUG,"%d: did a stat of %s\n",now,_PATH_UTMP);
		    
		    dd -> last_stat = now;

		    if((statbuf.st_mtime - dd->mtime) > 0)
			{
			    struct utmp *ut_rec; /* utmp record */
			    
			    /* we have to wade through utmp */
			    if(debug_level > 1)
				syslog(LOG_DEBUG,"%d: wading through utmp %s\n",now, _PATH_UTMP);
			    
			    dd -> mtime = statbuf.st_mtime;
			    
			    while ((ut_rec = getutent()))
				{
				    if ((ut_rec->ut_type == USER_PROCESS) &&
					(ut_rec->ut_name[0] != '\000') &&
					(strcmp(ut_rec->ut_line,line)==0))
					{
					    if(dd -> user) free(dd -> user);
					    dd -> user = malloc(10);
					    strncpy(dd -> user, ut_rec->ut_user, 8);
					    dd->user[8] = 0;
					    if(debug_level > 0)
						syslog(LOG_DEBUG,"found %s for %s\n",dd->user, line);
					    break;
					}
				}
			    endutent();
			}
		}
	    else
		{
		    syslog(LOG_ERR,"couldn't stat %s: %m\n",_PATH_UTMP);
		    return NULL;
		}
	}
    
    return dd -> user;
}

void do_acct()
{
  struct sockaddr saddr;
  int sizeaddr;
  
  unsigned char buff[1600];
  unsigned char *buf;

  int hardheader;
  int length;
  static struct iphdr *tmp_iphdr;
  int type;

  int do_user;
  char *user;

  buf = &buff[20];
  packets = malloc(sizeof(struct statistics));

  if(!packets)
      {
	  syslog(LOG_ERR,"out of memory");
	  daemon_stop(0);
      }
  packets->ignored = packets->netignored = packets->ip = packets->local = 0;
  packets->ip_icmp = packets->ip_tcp = packets->ip_udp = packets->ip_other = 0;
  packets->unenc = 0;

  ignorenet = cfg -> ignorenet;

  olist = plist = NULL;
  olistsize = plistsize = 0;
  lck = writing = dumping = 0;

  max_err_delay = cfg -> err_delay;
  err_delay = 0;

  list_lookups = list_compares = 0;

  may_write = 1;

  now = time(NULL);

  alarm(1);
  running=1;

  while (running)
    {
	sizeaddr = 14;
	length = recvfrom (capture_sd, buf, 127, 0, &saddr, &sizeaddr);
	if (length == -1)
	    {
		if(debug_level > 4)
		    {
			syslog(LOG_DEBUG,"recvfrom: %m\n");
		    }
		continue;
	    }

	do_user = 0;

	if((strncmp(saddr.sa_data,"eth",3)==0) || strncmp(saddr.sa_data,"lo",2)==0)
	    /* THIS NEEDS FIXING !! LIST IS INCOMPLETE */
	    {
		hardheader = 14;
		type = (buf[12] * 256 + buf[13]);
		
		if(type != ETH_P_IP)
		    {
			/* ETH_P_ARP, ETH_P_RARP, ETH_P_IPX, etc. */
			packets->ignored++;
			continue;
		    }
	    }
	else
	    {
		/* ASSUMES: interface - line just with ppp and slip etc. */
		do_user = 1;

		hardheader = 0;
		type = 0;
		packets->unenc++;
#ifdef IGNORE_UNENC
		continue; /* ignore ppp/slip */
#endif
	    }

	tmp_iphdr = (void *) (buf+hardheader);
	if((tmp_iphdr->saddr & cfg->ignoremask) == (tmp_iphdr->daddr & cfg->ignoremask))
	    {
		packets->local++;
		continue;
	    }
	else
	    {
		if(netignore(tmp_iphdr->saddr) || netignore(tmp_iphdr->daddr))
		    {
			if(debug_level > 3)
			    {
				char tmp[18];
				strcpy(tmp, intoa(tmp_iphdr->saddr));
				syslog(LOG_DEBUG,"netignored: %s -> %s\n",
				       tmp,intoa(tmp_iphdr->daddr));
			    }
			packets->netignored++;
			continue;
		    }
		else
		    {
			packets->ip++;
			user = (do_user !=0 ) ? check_user(saddr.sa_data) : NULL;
			handle_ip(buf+hardheader, saddr.sa_data, user);
		    }
	    }
    }
}

void handle_ip(unsigned char buf[], char *devname, char *user)
{
    static struct iphdr *tmp_iphdr;
    static struct tcphdr *tmp_tcphdr;
    unsigned short srcport, dstport;
    tmp_iphdr = (void *) &buf[0];
    tmp_tcphdr = (void *) &buf[tmp_iphdr->ihl*4];
    /* relevant headers of udphdr and tcphdr are identical */

    switch(tmp_iphdr->protocol)
	{
	case IP_UDP:
	    packets->ip_udp++;
	    
	    srcport = ntohs(tmp_tcphdr->source);
	    dstport = ntohs(tmp_tcphdr->dest);

	    break;
	case IP_TCP:
	    packets->ip_tcp++;

	    srcport = ntohs(tmp_tcphdr->source);
	    dstport = ntohs(tmp_tcphdr->dest);

	    break;
	case IP_ICMP:
	    packets->ip_icmp++;
	    srcport = dstport = 0;
	    break;
	default:
	    packets->ip_other++;
	    srcport = dstport = 0;
	    break;
	}
    
    register_packet(tmp_iphdr->saddr,tmp_iphdr->daddr,tmp_iphdr->protocol, srcport, dstport, ntohs(tmp_iphdr->tot_len), devname, user);
}

/* simplified version, performance ??? */
void register_packet(unsigned long int src,unsigned long int dst, unsigned char proto, unsigned short srcport, unsigned short dstport, int size, char *devname, char *user)
{
    if(lck==0)
	{
	    struct ipdata *p;
	    lck = 1;
	    p = plist;
	    list_lookups++;
	    while(p)
		{
		    list_compares++;
		    if( (p->proto == proto) && (p->src == src) && (p->dst == dst) &&  (p->srcport == srcport) && (p->dstport == dstport) && (strcmp(p->devname, devname)==0))
			{
			    p->bytes +=size;
			    p->when = now;
			    lck = 0;
			    return;
			}
		    p = p->next;
		}
	    p = malloc(sizeof(struct ipdata));
	    if(p == NULL)
		{
		    packets -> dropped++;
		    return;
		}
	    plistsize++;
	    p -> src = src;
	    p -> dst = dst;
	    p -> proto = proto;
	    p -> srcport = srcport;
	    p -> dstport = dstport;
	    p -> bytes = size;
	    p -> devname = strdup(devname);
	    p -> user = strdup((user == NULL) ? "unknown" : user);
	    p -> next = plist;
	    p -> when = now;
	    
	    plist = p;
	    lck = 0;
	}
    else
	{
	    packets->dropped++;
	}
}

int do_write_list(FILE *f, struct ipdata *list)
{
    struct ipdata *p;
    char tmp[20];

    p = list;

    while(p)
	{
	    strcpy(tmp, intoa(p->src));
	    if(fprintf(f, "%lu\t%d\t%s\t%d\t%s\t%d\t%lu\t%s\t%s\n",time(0),p->proto,tmp,p->srcport,intoa(p->dst),p->dstport,p->bytes,p->devname,p->user)<0)
		{
		    return 1;
		};
	    p = p->next;
	}

    if(fclose(f)<0)
	{
	    return 1;
	}

    return 0;
}

/* write and clear olist */
void write_list(void)
{
    FILE *f;
    char tmpn[255];

    while( (writepid = fork()) < 0) sleep(1);
    if (writepid!=0) return;

    /* Here goes the child */

    sprintf(tmpn, "/tmp/nacctd.write.%d", getpid());
    creat(tmpn, S_IRUSR);

    openlog("nacctd (write)", 0, LOG_DAEMON);
    if(debug_level > 2)
	{
	    syslog(LOG_DEBUG, "write process %d forked\n", getpid());
	}

    f = fopen(cfg->filename, "a");
    if(f==NULL)
	{
	    unlink(tmpn);
	    syslog(LOG_ERR, "error opening file %s: %m\n",cfg->filename);
	    exit(1);
	}

    if(do_write_list(f, olist) != 0)
	{
	    unlink(tmpn);
	    syslog(LOG_ERR, "error writing to file %s: %m\n", cfg->filename);
	    exit(1);
	}

    olist = NULL;

    unlink(tmpn);

    if(debug_level > 1)
	{
	    syslog(LOG_DEBUG, "write finished, count = %d\n", olistsize);
	}

    exit(0);
}

void dump_curr_list(void)
{
    FILE *f;
    char tmpn[255];

    while( (dumppid = fork()) < 0) sleep(1);
    if (dumppid!=0) return;

    /* Here goes the child */

    sprintf(tmpn, "/tmp/nacctd.dump.%d", getpid());
    creat(tmpn, S_IRUSR);

    openlog("nacctd (dump)", 0, LOG_DAEMON);
    if(debug_level > 2)
	{
	    syslog(LOG_DEBUG, "dump process %d forked\n", getpid());
	}

    if(plistsize == 0)
	{
	    unlink(tmpn);
	    unlink(cfg->dumpname);
	    if(debug_level > 1)
		syslog(LOG_DEBUG, "dump finished, dump empty\n");
	    exit(0);
	}

    f = fopen(cfg->dumpname, "w");
    if(f==NULL)
	{
	    unlink(tmpn);
	    syslog(LOG_ERR, "error opening file %s: %m\n",cfg->dumpname);
	    exit(1);
	}

    if(do_write_list(f, plist) != 0)
	{
	    unlink(tmpn);
	    syslog(LOG_ERR, "error writing to file %s: %m\n", cfg->dumpname);
	    exit(1);
	}

    plist = NULL;

    unlink(tmpn);

    if(debug_level > 1)
	{
	    syslog(LOG_DEBUG, "dump finished, count = %d\n", plistsize);
	}
    exit(0);
}


void child_finished(int sig)
{
    int status;
    pid_t pid;

    if(debug_level > 2)
	{
	    syslog(LOG_DEBUG, "SIGCHLD received\n");
	}


    while((pid = waitpid(-1, &status, WNOHANG)) != 0)
	{
	    if(errno == ECHILD)
		break; /* no child processes */

	    if((pid == writepid) || (pid == dumppid))
		{
		    if(WIFEXITED(status))
			{
			    if(WEXITSTATUS(status)==0)
				{
				    if(pid == writepid)
					writing = 0;
				    else
					dumping = 0;
				}
			    else
				{
				    syslog(LOG_ERR, 
					   "child %d exited with error status %d.\n",
					   pid, WEXITSTATUS(status));
				    if(pid == writepid)
					{
					    err_delay = max_err_delay;
					    writing = 0;
					}
				    else
					dumping = 0;
				}
			}
		    else
			{
			    syslog(LOG_ERR,
				   "Huh? Child %d terminated or stopped by signal (%m)\n",
				   pid);
			    if(pid == writepid)
				writing = 0;
			    else
				dumping = 0;
			}
		}
	    else
		{
		    syslog(LOG_ERR, "Huh? Child (%d) returned, but not the one we expected (%d, %d)!\n", pid, writepid, dumppid);
		}
	    if(debug_level > 2)
		{
		    syslog(LOG_DEBUG, "child %d signaled return\n",pid);
		}
	}
}

void alarm_handler(int sig)
{
    static time_t last_check = 0;
    static time_t next_write_log = 0;

    if(debug_level > 2)
	{
	    syslog(LOG_DEBUG,"alarm striked\n");
	}

    now++;

    if((now - last_check) > 60)
	{
	    time_t nnow;
	    
	    nnow = time(NULL);
	    if(nnow!=now)
		{
		    if(((nnow - now) > 5) && (debug_level > 2))
			{
			    syslog(LOG_DEBUG,"internal clock corrected (off by %d seconds)\n",nnow-now);
			}
		    now = nnow;
		}
	    last_check = now;
	}

    if(now >= next_write_log)
	{
	    write_log(0);
	    next_write_log = now + cfg -> flush;
	}

    alarm(1);
}

void write_log(int force)
{
    struct ipdata *p, *q;
    static struct ipdata *tlist; /* temp */

    if(debug_level > 2)
	{
	    syslog(LOG_DEBUG,"write_log called\n");
	}

    if(err_delay!=0)
	{
	    err_delay--;
	    syslog(LOG_INFO,"flushing delayed due to error\n");
	}
    else if((writing == 0) && (lck == 0) && (may_write == 1)) /* delay if another write cycle is still in progress */
	{

	    if(debug_level > 0)
		{
		    syslog(LOG_DEBUG, 
			   "ignored: %ld netignored: %ld local:%ld ip:%ld unenc:%ld dropped:%ld\n", 
			   packets->ignored, packets->netignored, packets->local, packets->ip, 
			   packets->unenc, packets->dropped);
		    syslog(LOG_DEBUG, 
			   "udp: %ld tcp:%ld icmp:%ld other:%ld\n", 
			   packets->ip_udp, packets->ip_tcp, packets->ip_icmp, 
			   packets->ip_other);
		    if(list_lookups != 0)
			{
			    syslog(LOG_DEBUG,
				   "lookups:%ld compares:%ld compares/lookup:%f\n",
				   list_lookups, list_compares, 
				   ((float) list_compares / (float) list_lookups));
			}
		}
	    lck = 1; /* can't update the list now */
	    
	    if(debug_level > 2)
		syslog(LOG_DEBUG, "Total of %d entries\n", plistsize);

	    /* We build two lists:
	       1) olist, which will be written out
	       2) tlist, which will be the new plist
	     */

	    p = plist;
	    tlist = NULL;
	    olist = NULL;
	    olistsize = 0;
	    plistsize = 0;
	    
	    while(p)
		{
		    q = p->next;
		    if(((now - p->when) > cfg->fdelay) || force)
			{
			    p->next = olist;
			    olist = p;
			    olistsize++;
			}
		    else
			{
			    p->next = tlist;
			    tlist = p;
			    plistsize++;
			}
		    p = q;
		}

	    plist = tlist;

	    if(dumping == 0)
		{
		    dumping = 1;
		    dump_curr_list();
		    if(debug_level > 2)
			syslog(LOG_DEBUG, "dumppid is %d\n", dumppid);
		}

	    writing = 1; /* no further writing 'til this is finished */
	    lck = 0;

	    if(debug_level > 2)
		syslog(LOG_DEBUG, "Split into %d [hold] and %d [write] = %d [total] entries\n", plistsize, olistsize, plistsize + olistsize);

	    write_list();
	    if(debug_level > 2)
		syslog(LOG_DEBUG, "writepid is %d\n", writepid);
	    
	    p=olist;
	    while(p)
		{
		    olist = p->next;
		    free(p->devname);
		    free(p->user);
		    free(p);
		    p=olist;
		}
	    if(debug_level > 2)
		{
		    syslog(LOG_DEBUG,"done freeing\n");
		}
	}
    else
	{
	    if(debug_level > 1)
		{
		    syslog(LOG_INFO,"flushing delayed\n");
		}
	}


}

void signal_debug(int sig)
{
    
    if(sig==SIGUSR1)
	{
	    debug_level++;
	    syslog(LOG_DEBUG,"setting debugging level to %d\n", debug_level);
	    
	}
    else if(sig==SIGUSR2)
	{
	    debug_level = 0;
	    syslog(LOG_DEBUG,"turning off debugging\n");
	}
    else if(sig==SIGWINCH)
	{
	    syslog(LOG_DEBUG,"nacctd, revisions:\n%s\n%s\n%s\n%s\n", 
		   rcs_revision_main_c, rcs_revision_capture_c,
		   rcs_revision_config_c, rcs_revision_daemon_c);
	}
    else if(sig==SIGTSTP)
	{
	    syslog(LOG_DEBUG,"received SIGTSTP\n");
	    may_write = 0;
	}
    else if(sig==SIGCONT)
	{
	    syslog(LOG_DEBUG,"received SIGCONT\n");
	    may_write = 1;
	}
    else if(sig==SIGIOT)
	{
	    syslog(LOG_DEBUG,"reopening socket\n");
	    reopen_socket();
	}
    else
	{
	    syslog(LOG_INFO,"signal_debug received signal %d, this can't happen\n");
	}
}

void signal_ignore(int sig)
{
    if(debug_level > 1)
	{
	    syslog(LOG_DEBUG,"got signal %d, ignoring\n", sig);
	}
}



