/**************************************************
OpengateM - a MAC address authentication system
daemon main module 

Copyright (C) 2011 Opengate Project Team
Written by Yoshiaki Watanabe

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
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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

Email: watanaby@is.saga-u.ac.jp
**************************************************/

/*************************************
This program uses following data structures to maintain the state of each terminal.
 * MAC address is used as the main key for recognizing the user terminal.
 * IP address is kept only for information.
 * The packet cache uses the MAC-IP pair, as to record all IPs. 

   1. Packet Check Cache
       To speed up the packet check process, the address checked once 
         is ignored for a while.
       The cache (key: MAC&IP address pair) is used to decide 
         the necessity of checking.
       The cache maintains the information of arrived packets 
         by using hash-table and queue in the memory of the local machine.
   2. Session Table
       The table (key: MAC address) maintains temporal information 
         of terminals allowing the use of network at now. 
       The data are stored in the work DB(SQLite3) on the local machine
       (table:sessionmd in db:opengatemd.db).
   3. MAC-IP pair Table
       The table maintains the MAC-IP pairs where an active session has the MAC
       (One MAC corresponds to plural IPs(ipv4 and ipv6))
       The data are stored in the work DB on the local machine.
       (table:macippair in db:opengatemd.db)
       The mac-ip pair table in management db is used only for logging.
   4. MAC address registration Table
       The table (key: MAC address) maintains MAC addresses of terminals
         and the owners' information.
       The data are stored in the management DB(MySQL) on a central machine
         and accessed via network.
       (table: macaddrs in db:opengatem)
   5. Cache of MAC address registration Table
       As the network DB access is time-consuming, the access cache 
         (key: MAC address) is maintained in the memory of the local machine.


*************************************/

#include	"opengatemd.h"

void sigHupHandler(int sig);
void sigIoHandler(int sig);
void sigTermHandler(int sig);

int sigHupArrived=FALSE;
int sigIoArrived=FALSE;

/*********************/
/*  main routine     */
/*********************/
int  main(int argc, char **argv)
{
  char ipAddress[ADDRMAXLN];  /* packet source ip address */
  char macAddress[ADDRMAXLN]; /* packet source mac address */
    unsigned char macAndIpAddressRaw[ADDRMAXLN];/* mac&ip addr in raw form */
  /* above is network raw binary concatenating MAC(6bytes) and IP(4or16Bytes) */
  int addrLen;               /* ip address byte length 6+4 or 6+16 */         

  char userId[USERMAXLN];     /* user id related to the mac address */
  char extraId[USERMAXLN];    /* optional id for the user */
  int macFound;               /* flag: mac address is resistered in db */
  int sessionFound;           /* flag: session for the address exists */
  int consoleMode=FALSE;      /* flag: start with console mode option */
  int endServiceMode=FALSE;   /* flag: start with end service option */
  int reloadServiceMode=FALSE;/* flag: start with reload option */ 
  int stopServiceMode=FALSE;  /* flag: start with stop service option */ 
  int showVersionMode=FALSE;  /* flag: show version */
  int helpMode=FALSE;         /* flag: start with help mode */

  int ttl;                    /* packet ttl(time to live) or hlim(hop limit) */
  int i;                      /* for loop control */
  int uselessCheckTime=0;     /* the last time for useless check */
  int checkInterval;          /* useless session check interval */
  int isNatOrRouter=FALSE;    /* the packet is sent from nat/router */
  char macAddrInUdp[ADDRMAXLN]; /* mac address sent from udp port */
  int ret;                         /* return code */
  char clientIpAddress[ADDRMAXLN]; /* udp client ip address */

  /* analyze arguments */
  for(i=1; i<argc; i++){
    if(*argv[i]=='-' && *(argv[i]+1)=='c') consoleMode=TRUE;
    else if(*argv[i]=='-' && *(argv[i]+1)=='e') endServiceMode=TRUE;
    else if(*argv[i]=='-' && *(argv[i]+1)=='r') reloadServiceMode=TRUE;
    else if(*argv[i]=='-' && *(argv[i]+1)=='s') stopServiceMode=TRUE;
    else if(*argv[i]=='-' && *(argv[i]+1)=='v') showVersionMode=TRUE;
    else helpMode=TRUE;
  }

  /* if unknown args, show help and exit */
  if(helpMode){
    ShowHelp(argv[0]);
    terminateProg(0);
  }

  /* if '-v' option, show 'makedir' and exit */
  /* makedir is the directory where the make command is executed */
  /* the directory can include the string such as 'opengatem0.9.9-trial'. */
  if(showVersionMode){
    printf("makedir: %s\n", MAKEDIR);
    terminateProg(0);
  }

  /* drop root privilege */
  seteuid(getuid());

  /* prepare config file */
  if(OpenConfFile()==-1) terminateProg(0);
 
  /* start log */
  errToSyslog(atoi(GetConfValue("Syslog/Enable")));
  openlog(GetConfValue("MacCheckDaemon"), LOG_PID, atoi(GetConfValue("Syslog/Facility")));

  /* initialize config */
  InitConf();

  /* if reloadServiceMode, send HUP to resident daemon */
  if(reloadServiceMode) ReloadDaemon();

  /* if stopServiceMode, stop resident deamon and this process */
  if(stopServiceMode){
    KillDaemon();
    terminateProg(0);
  }

  /* if endServiceMode, stop deamon */
  /*  and close sessions(about 30-lines below in this source) */
  if(endServiceMode) KillDaemon();

  /* if not console mode, run as daemon */
  if(consoleMode)  errToSyslog(FALSE);    /* console mode (no syslog) */
  else   Daemonize();                     /* daemon mode (fork&exit) */

  /* set lock file to prevent overlapped execution */
  if(!LockDaemonLockFile()){
    terminateProg(0);
  }

  /* set signal functions */
  signalx(SIGHUP, sigHupHandler);
  signalx(SIGTERM, sigTermHandler);

  /* initializing */
  InitPcap();
  InitCache();
  InitMacCache();
  InitWorkDb();
  if(!InitMngDb()) terminateProg(0);
  InitTtlCheck();
  InitWatchlistCache();
  PrepareUdpPort(sigIoHandler); /* UDP port runs in asynchronous mode */

  /* if endServiceMode is indicated, close all sessions, and exit */
  if(endServiceMode){
    DelAllSessions();
    terminateProg(0);
  }
  
  /* set check interval and remove useless residue sessions */
  checkInterval=atoi(GetConfValue("UselessCheckInterval"));
  uselessCheckTime=time(NULL);
  DelUselessSessions();

  /*** enter infinite loop of packet inspection ***/
  while(1){

    /* if sig-hup flag is on, reload this program */
    /* sig-hup flag is set in sigHupHandler, when HUP signal arrives */
    if(sigHupArrived)execlp(argv[0], argv[0], NULL);

    /* if sig-IO flag is on, get mac addresses from UDP port */
    /* and remove the addresses from caches (renew info at next capture) */
    /* sig-IO flag is on in sigIoHandler, when a packet arrives. */
    /* the packet includes renewed mac addresses sent from management program. */ 
    if(sigIoArrived){
      sigIoArrived=FALSE;
      while(GetDataFromUdpPort(macAddrInUdp, ADDRMAXLN, clientIpAddress)>0){
	if(IsUdpClientTrusted(clientIpAddress)){
	  DelCacheItem(macAddrInUdp,"");
	  DelMacCacheItem(macAddrInUdp);
	}
      }
    }      

    /* get one packet from pcap */
    ret=GetNextPacketFromPcap(macAndIpAddressRaw, &addrLen, &ttl);

    /* if no packet */
    if(ret==0){
 
      /* when long time passed from previous check, check&delete useless sessions */
      if( time(NULL) - uselessCheckTime > checkInterval ){
	uselessCheckTime = time(NULL);
	DelUselessSessions();
      }

      /* and return to loop top */
      continue;
    }

    /* ignore not-ip packet */
    if(ret==-1) continue;

    /* ignore local packet */
    if(ttl<=1) continue;
   
    /* ignore the packet tha is checked recently */
    if( IsRecentlyCheckedAddress(macAndIpAddressRaw, addrLen) ) continue;

    /**** no more processing for recently checked packets ****/
    /**** only cache timeout packets proceeds to below ****/

    /* convert address from network-raw form to presentation form */
    ConvertMacFromRawToDisplay(macAndIpAddressRaw, macAddress);
    ConvertIpFromRawToDisplay(macAndIpAddressRaw+MACADDRLN,
			      addrLen-MACADDRLN, ipAddress);

    /* if the address is included in watchlist, report the detection */
    /* watchlist is the address list needing specific reporting */
    /* (e.g., suspicion of illegal access). no need for normal operation */
    if(IsFoundInWatchlistCache(macAddress)==TRUE){
      err_msg_warn("WARN: find mac=%s ip=%s", macAddress, ipAddress);
    }

    /* check NAT/Router and save info to db */
    /* when NAT/Router is inserted, the acquired MAC is the address of NAT/Router */
    /* thus the MAC based control is failed */
    /* at now, not denying the packet but reporting the detection. */
    isNatOrRouter=IsSentViaNatOrRouter(ipAddress, macAddress, ttl);
    if(isNatOrRouter) PutLogAtNatOrRouter(isNatOrRouter,ipAddress,macAddress,ttl);
    PutMacInfoToWorkDb(macAddress, ttl, isNatOrRouter);

    /*** get the status of the terminal from session table and DB ***/

    /* search the captured address in session table */
    sessionFound = IsMatchedSessionFound(macAddress);

    /* search the captured address in cache of MAC DB */ 
    macFound = QueryMacFromMacCache(macAddress, userId, extraId);

    /* if failed, access MAC DB */
    if(macFound==ERROR){
      macFound = QueryMacFromMngDb(macAddress, userId, extraId);
      
      /* if db access is failed, set not-found and retry at next capture */
      /* if db access is successed (found or not-found), save it to cache */ 
      if(macFound==ERROR) macFound=FALSE;
      else AddMacCacheItem(macAddress, userId, extraId, macFound);
    }

    /*** depending the status, add/del/renew the session ***/

    /* if valid mac and no session, start session */
    if(macFound && !sessionFound){
	AddSession(macAddress, userId, extraId);

	/* save MAC and IP address pair to work db */
	SetMacIpPair(macAddress, ipAddress, userId, extraId);
    }

    /* if no mac and started session, stop session */
    /* (MAC-IP pair in work db is removed in delSession) */
    if(!macFound && sessionFound){
      DelSession(macAddress);
    }

    /* if valid mac and started session, renew check time */
    if(macFound && sessionFound){

      /* in normal case, ipfw rule exists. */
      if(IsMacAddressFoundInIpfw(macAddress)){
	RenewSession(macAddress);
      }
      
      /* when no ipfw rule exists, reset the session */
      else{
	DelSession(macAddress);
	AddSession(macAddress, userId, extraId);	
      }

      /* save MAC-IP pair to work db (new ip is found for a mac in session).  */
      SetMacIpPair(macAddress, ipAddress, userId, extraId);
    }

    /*  check useless sessions at some interval */
    /* (MAC and IP pairs are removed in stop session) */
    if( time(NULL) - uselessCheckTime > checkInterval ){
      uselessCheckTime = time(NULL);
      DelUselessSessions();
    }
  }
  /*** end of infinite loop ***/

  /* clear data structures (can't reach here, but coded for debugging) */
  FreeCache();
  FreeMacCache();
  FreeWatchlistCache();
  ClosePcap();
  CloseConfFile();
  CloseMngDb();

  terminateProg(0);
  return 0;
}

/********************************
 open and lock proc lock file 
 to prevent overlapped daemon exec
********************************/
int lockDaemonLockFile(void){
  
  static int lockFd;
  char str[WORDMAXLN];
 
  /* open process lock file */
  lockFd = open(GetConfValue("DaemonLockFile"), O_RDWR|O_CREAT, 0644);
  
  /* if cannot open or cannot lock, return false */
  if(lockFd<0){
    err_msg("ERR at %s#%d: cannot open daemon lock file:%s",__FILE__,__LINE__,
	    lockFd);
    return FALSE;
  }
  if(lockf(lockFd, F_TLOCK, 0)<0) return FALSE;

  /* record pid */
  snprintf(str, WORDMAXLN, "%d", getpid());
  write(lockFd, str, strlen(str)+1);

  /* normally locked */
  return TRUE;
}


/******************************
daemonize the process
******************************/
void daemonize(void){

  int pid;
  int i;

  /* detach from parent */
  pid = fork();
  if(pid < 0){
   err_msg("ERR at %s#%d: cannot fork",__FILE__,__LINE__);
    terminateProg(0);
  }

  /* parent process exits */
  if(pid > 0) exit(0);

  /* child process runs from here */
  /* set new prosess group */
  setsid();

  /* close all file descriptors */
  for(i=getdtablesize(); i>=0; i--) close(i);

  /* connect stdin/out/err to null */
  i=open("/dev/null", O_RDWR); dup(i); dup(i);

  /* set newly created file permission */
  /* umask(033);   */

  if(debug>0) err_msg("INFO: Deamon started");
}

/****************************************
show help message
****************************************/
void showHelp(char* procName){
   printf("\n");
   printf("firewall control by mac address\n");
   printf(" see detail in Opengate Homepage\n");
   printf("\n");
   printf(" format: %s [arg] \n", procName);
   printf(" arg : -c = run on console (run as daemon in default)\n");
   printf("     : -e = close all sessions and end service\n");
   printf("     : -r = reload deamon(not close sessions)\n");
   printf("     : -s = stop deamon (not close sessions)\n");
   printf("     : -v = show make dir to check version\n");
 }

/*********************************
signal handler for SIGIO
*********************************/
void sigIoHandler(int sig){

  sigIoArrived=TRUE;
}

/*********************************
signal handler for SIGHUP
*********************************/
void sigHupHandler(int sig){

  if(debug>0)err_msg("INFO: Deamon receives HUP signal");
  sigHupArrived=TRUE;
}

/*********************************
signal handler for SIGTERM
*********************************/
void sigTermHandler(int sig){

  if(debug>0)err_msg("INFO: Deamon receives TERM signal");
  terminateProg(0);
}

/*********************************
kill daemon process
*********************************/
void killDaemon(void){
  FILE* file=NULL;
  struct stat st;
  int pid=0;
  char* lockFileMd;

  /* get lock file name */
  lockFileMd=GetConfValue("DaemonLockFile");

  /* if lock file does not exist, skip */
  if(stat(lockFileMd, &st)!=0){
    ;
  }

  /*else (file exists), read pid from the file */
  else if((file=fopen(lockFileMd, "r"))==NULL){
    err_msg("ERR at %s#%d: cannot open proc lock file:%s",__FILE__,__LINE__
	    ,lockFileMd);
  }
  else if(fscanf(file, "%d", &pid)==0){
    err_msg("ERR at %s#%d: cannot read proc lock file:%s",__FILE__,__LINE__
	    ,lockFileMd);
  }
  if(file!=NULL) fclose(file);

  /* send kill signal to the 'pid' process */
  if(pid!=0){
    seteuid(0);   /* get root privilege */
    kill(pid, SIGKILL);
    seteuid(getuid());   /* drop root privilege */
  }

  /* remove the lockfile */
  remove(lockFileMd);
}

/*********************************
reload daemon process
*********************************/
void reloadDaemon(void){
  FILE* file=NULL;
  struct stat st;
  int pid=0;
  char* lockFileMd;

  /* get lock file name */
  lockFileMd=GetConfValue("DaemonLockFile");

  /* if lock file does not exists, skip */
  if(stat(lockFileMd, &st)!=0){
    ;
  }

  /* else (file exists), read pid from the file */
  else if((file=fopen(lockFileMd, "r"))==NULL){
    err_msg("ERR at %s#%d: cannot open proc lock file:%s",__FILE__,__LINE__
	    ,lockFileMd);
  }
  else if(fscanf(file, "%d", &pid)==0){
    err_msg("ERR at %s#%d: cannot read proc lock file:%s",__FILE__,__LINE__
	    ,lockFileMd);
  }
  if(file!=NULL)fclose(file);

  /* send hup signal to the 'pid' process */
  if(pid!=0){
    seteuid(0);   /* get root privilege */
    kill(pid, SIGHUP);
    seteuid(getuid());   /* drop root privilege */
  }
}

/*************************************
put out end message and exit 
*************************************/
void terminateProg(int ret){

  /* close work db (opengatemd.db) */
  FinalizeWorkDb();

  if(debug>0) err_msg("INFO: Terminated");
  exit(ret);
}


/************************************
 routines for debugging output
 ***********************************/
int LockDaemonLockFile(void){
  int ret;
  if(debug>1) err_msg("DEBUG:=>lockDaemonLockFile( )");
  ret = lockDaemonLockFile();
  if(debug>1) err_msg("DEBUG:(%d)<=lockDaemonLockFile( )",ret);
  return ret;
}

void Daemonize(void){
  if(debug>1) err_msg("DEBUG:=>daemonize( )");
  daemonize();
  if(debug>1) err_msg("DEBUG:<=daemonize( )");
}

void ShowHelp(char* procName){
  if(debug>1) err_msg("DEBUG:=>showHelp(%s)", procName);
  showHelp(procName);
  if(debug>1) err_msg("DEBUG:<=showhelp( )");
}
void KillDaemon(void){
  if(debug>1) err_msg("DEBUG:=>killDaemon( )");
  killDaemon();
  if(debug>1) err_msg("DEBUG:<=killDaemon( )");
}
void ReloadDaemon(void){
  if(debug>1) err_msg("DEBUG:=>reloadDaemon( )");
  reloadDaemon();
  if(debug>1) err_msg("DEBUG:<=reloadDaemon( )");
}
