/*
 * Tentun - covert tunneling project
 *
 * Copyright (C) 2003 David Gordon
 *
 *      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.
 */

#include "config.h"
 
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include <time.h>

#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif

#ifdef HAVE_SCHED_H
#include <sched.h>
#endif

#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif

#ifdef HAVE_NETINET_TCP_H
#include <netinet/tcp.h>
#endif

#include "tentun.h"
#include "tunnel.h"
#include "plugins.h"
#include "lib.h"
#include "driver.h"


/* Termination flag */
static volatile sig_atomic_t linker_term;

/* io safety flag */
volatile sig_atomic_t __io_canceled = 0;


int (*dev_write)(int fd, char *buf, int len);
int (*dev_read)(int fd, char *buf, int len);

int (*proto_write)(int fd, char *buf, int len);
int (*proto_read)(int fd, char *buf);



static void sig_term(int sig)
{
     TENTUN_PRINT("Closing connection\n");
     io_cancel();
     linker_term = TENTUN_SIG_TERM;
}

static void sig_hup(int sig)
{
     TENTUN_PRINT("Reestablishing connection\n");
     io_cancel();
     linker_term = TENTUN_SIG_HUP;
}

int sock_loop(struct tentun_host *host)
{
     int fd1 = host->rmt_fd;
     int fd2 = host->loc_fd; 
     register int len, fl;
     struct timeval tv;
     char *buf, *out;
     fd_set fdset;
     int maxfd, idle = 0;

     if( !(buf = (char*)plug_alloc(TENTUN_FRAME_SIZE + TENTUN_FRAME_OVERHEAD)) ){
	TENTUN_PRINT("Can't allocate buffer for the linker\n"); 
        return 0; 
     }

     proto_write(fd1, buf, TENTUN_ECHO_REQ);

     maxfd = (fd1 > fd2 ? fd1 : fd2) + 1;

     linker_term = 0;
     while( !linker_term ){
	errno = 0;

        /* Wait for data */
        FD_ZERO(&fdset);
	FD_SET(fd1, &fdset);
	FD_SET(fd2, &fdset);

 	tv.tv_sec  = host->ka_interval;
	tv.tv_usec = 0;

	if( (len = select(maxfd, &fdset, NULL, NULL, &tv)) < 0 ){
	   if( errno != EAGAIN && errno != EINTR )
	      break;
	   else
	      continue;
	} 
	
	if( !len ){
	   /* We are idle, lets check connection */
	   if( host->flags & TENTUN_KEEP_ALIVE ){
	      if( ++idle > host->ka_failure ){
	         TENTUN_PRINT("Session %s network timeout\n", host->host);
		 break;	
	      }
	      /* Send ECHO request */
	      if( proto_write(fd1, buf, TENTUN_ECHO_REQ) < 0 )
		 break;
	   }
	   continue;
	}	   

	/* Read frames from network(fd1), decode and pass them to 
         * the local device (fd2) */
	if( FD_ISSET(fd1, &fdset) && plug_check_up() ){
	   idle = 0; 
	   if( (len=proto_read(fd1, buf)) <= 0 )
	      break;

	   /* Handle frame flags */
	   fl = len & ~TENTUN_FSIZE_MASK;
           len = len & TENTUN_FSIZE_MASK;
	   if( fl ){
	      if( fl==TENTUN_BAD_FRAME ){
		 TENTUN_PRINT("Received bad frame\n");
		 continue;
	      }
	      if( fl==TENTUN_ECHO_REQ ){
		 /* Send ECHO reply */
	 	 if( proto_write(fd1, buf, TENTUN_ECHO_REP) < 0 )
		    break;
		 continue;
	      }
   	      if( fl==TENTUN_ECHO_REP ){
		 /* Just ignore ECHO reply */
		 continue;
	      }
	      if( fl==TENTUN_CONN_CLOSE ){
	         TENTUN_PRINT("Connection closed by other side\n");
		 break;
	      }
	   }   

	   if( (len=plug_run_up(len,buf,&out)) == -1 )
	      break;	
	   if( len && dev_write(fd2,out,len) < 0 ){
              if( errno != EAGAIN && errno != EINTR )
                 break;
              else
                 continue;
           }
	}

	/* Read data from the local device(fd2), encode and pass it to 
         * the network (fd1) */
	if( FD_ISSET(fd2, &fdset) && plug_check_down() ){
	   if( (len = dev_read(fd2, buf, TENTUN_FRAME_SIZE)) < 0 ){
	      if( errno != EAGAIN && errno != EINTR )
	         break;
	      else
		 continue;
	   }
	   if( !len ) break;
	
	   if( (len=plug_run_down(len,buf,&out)) == -1 )
	      break;
	   if( len && proto_write(fd1, out, len) < 0 )
	      break;
	}
     }
     if( !linker_term && errno )
	TENTUN_PRINT("%s (%d)\n", strerror(errno), errno);

     if (linker_term == TENTUN_SIG_TERM) {
       host->persist = 0;
     }

     /* Notify other end about our close */
     proto_write(fd1, buf, TENTUN_CONN_CLOSE);
     plug_free(buf);

     return 0;
}



int linkfd(struct tentun_host *host)
{
     struct sigaction sa, sa_oldterm, sa_oldint, sa_oldhup;
     int old_prio;

     old_prio=getpriority(PRIO_PROCESS,0);
     setpriority(PRIO_PROCESS,0,TUNNEL_PRIO);

     /* Build modules stack */
/*
     if(host->flags & TENTUN_ZLIB)
	plug_add_mod(&lfd_zlib);

     if(host->flags & TENTUN_LZO)
	plug_add_mod(&lfd_lzo);

     if(host->flags & TENTUN_ENCRYPT)
	plug_add_mod(&lfd_encrypt);

     if(host->flags & TENTUN_SHAPE)
	plug_add_mod(&lfd_shaper);
*/

/* TODO:
 * Implement mutually exclusive modules
 */
     if(host->flags & TENTUN_S_HTTP)
       plug_add_mod(&plug_simple_http);

     if(host->flags & TENTUN_DNS)
       plug_add_mod(&plug_dns);

     if(plug_alloc_mod(host))
	return 0;

     memset(&sa, 0, sizeof(sa));
     sa.sa_handler=sig_term;
     sigaction(SIGTERM,&sa,&sa_oldterm);
     sigaction(SIGINT,&sa,&sa_oldint);
     sa.sa_handler=sig_hup;
     sigaction(SIGHUP,&sa,&sa_oldhup);

     io_init();

     sock_loop(host);

     plug_free_mod();
     
     sigaction(SIGTERM,&sa_oldterm,NULL);
     sigaction(SIGINT,&sa_oldint,NULL);
     sigaction(SIGHUP,&sa_oldhup,NULL);

     setpriority(PRIO_PROCESS,0,old_prio);

     return linker_term;
}


/* Initialize and start the tunnel.
   Returns:
      -1 - critical error
      0  - normal close or noncritical error 
*/
   

int tunnel_exec(struct tentun_host *host)
{
     int null_fd, pid, opt;
     int fd[2]={-1, -1};
     char dev[TENTUN_DEV_LEN]="";
     int interface_already_open = 0;

     if ( (host->persist == TENTUN_PERSIST_KEEPIF) &&
	  (host->loc_fd >= 0) )
        interface_already_open = 1;

     /* Initialize device. */
     if( host->dev ){
        strncpy(dev, host->dev, TENTUN_DEV_LEN);
	dev[TENTUN_DEV_LEN-1]='\0';
     }
     if( ! interface_already_open ){
        switch( host->flags & TENTUN_TYPE_MASK ){
           case TENTUN_TTY:
	      if( (fd[0]=pty_open(dev)) < 0 ){
		 TENTUN_PRINT("Can't allocate pseudo tty. %s(%d)\n", strerror(errno), errno);
		 return -1;
	      }
	      break;

           case TENTUN_PIPE:
	      if( pipe_open(fd) < 0 ){
		 TENTUN_PRINT("Can't create pipe. %s(%d)\n", strerror(errno), errno);
		 return -1;
	      }
	      break;

           case TENTUN_ETHER:
	      if( (fd[0]=tap_open(dev)) < 0 ){
		 TENTUN_PRINT("Can't allocate tap device %s. %s(%d)\n", dev, strerror(errno), errno);
		 return -1;
	      }
	      break;

	   case TENTUN_TUN:
	      if( (fd[0]=tun_open(dev)) < 0 ){
		 TENTUN_PRINT("Can't allocate tun device %s. %s(%d)\n", dev, strerror(errno), errno);
		 return -1;
	      }
	      break;
	}
	host->loc_fd = fd[0];
     }
     host->sopt.dev = strdup(dev);

     /* Initialize protocol. */
     switch( host->flags & TENTUN_PROT_MASK ){
        case TENTUN_TCP:
	   opt=1;
	   setsockopt(host->rmt_fd,SOL_SOCKET,SO_KEEPALIVE,&opt,sizeof(opt) );

	   opt=1;
	   setsockopt(host->rmt_fd,IPPROTO_TCP,TCP_NODELAY,&opt,sizeof(opt) );

	   proto_write = tcp_write;
	   proto_read  = tcp_read;

	   break;

        case TENTUN_UDP:
	   if( (opt = udp_session(host)) == -1){
	      TENTUN_PRINT("Can't establish UDP session\n");
	      close(fd[1]);
	      if( ! ( host->persist == TENTUN_PERSIST_KEEPIF ) )
		 close(fd[0]);
	      return 0;
	   } 	

 	   proto_write = udp_write;
	   proto_read = udp_read;

	   break;
     }

     if( ! interface_already_open ){
        /* do this only the first time when in persist = keep mode */
        switch( (pid=fork()) ){
	   case -1:
	      TENTUN_PRINT("Couldn't fork()\n");
	      if( ! ( host->persist == TENTUN_PERSIST_KEEPIF ) )
		 close(fd[0]);
	      close(fd[1]);
	      return 0;
 	   case 0:
	      switch( host->flags & TENTUN_TYPE_MASK ){
	         case TENTUN_TTY:
		    /* Open pty slave (becomes controlling terminal) */
		    if( (fd[1] = open(dev, O_RDWR)) < 0){
		       TENTUN_PRINT("Couldn't open slave pty\n");
		       exit(0);
		    }
		    /* Fall through */
	         case TENTUN_PIPE:
		    null_fd = open("/dev/null", O_RDWR);
		    close(fd[0]);
		    close(0); dup(fd[1]);
		    close(1); dup(fd[1]);
		    close(fd[1]);

		    /* Route stderr to /dev/null */
		    close(2); dup(null_fd);
		    close(null_fd);
		    break;
	         case TENTUN_ETHER:
	         case TENTUN_TUN:
		    break;
	      }

	      exit(0);           
	}
     }

     switch( host->flags & TENTUN_TYPE_MASK ){
        case TENTUN_TTY:
	   dev_read  = pty_read;
	   dev_write = pty_write; 
	   break;

        case TENTUN_PIPE:
	   /* Close second end of the pipe */
	   close(fd[1]);

	   dev_read  = pipe_read;
	   dev_write = pipe_write; 
	   break;

        case TENTUN_ETHER:
	   dev_read  = tap_read;
	   dev_write = tap_write; 
	   break;

        case TENTUN_TUN:
	   dev_read  = tun_read;
	   dev_write = tun_write; 
	   break;
     }

     opt = linkfd(host);

     if(! ( host->persist == TENTUN_PERSIST_KEEPIF ) ) {
	/* Gracefully destroy interface */
	switch( host->flags & TENTUN_TYPE_MASK ){
           case TENTUN_TUN:
	      tun_close(fd[0], dev);
	      break;

           case TENTUN_ETHER:
	      tap_close(fd[0], dev);
	      break;
	}

       	close(host->loc_fd);
     }

     /* Close all other fds */
     close(host->rmt_fd);
     close(fd[1]);

     return opt;
}
