/* parprouted: ProxyARP routing daemon. 
 * (C) 2008 Vladimir Ivaschenko <vi@maks.net>
 *
 * This application is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License.
 *
 * This software 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 */
 
#include "parprouted.h"

char *progname;
int debug=0;
int verbose=0;
int option_arpperm=0;
static int perform_shutdown=0;

char *errstr;

pthread_t my_threads[MAX_IFACES+1];
int last_thread_idx=-1;

char * ifaces[MAX_IFACES];
int last_iface_idx=-1;

ARPTAB_ENTRY **arptab;
pthread_mutex_t arptab_mutex;

ARPTAB_ENTRY * replace_entry(struct in_addr ipaddr, char *dev) 
{
    ARPTAB_ENTRY * cur_entry=*arptab;
    ARPTAB_ENTRY * prev_entry=NULL;

    while (cur_entry != NULL && ( ipaddr.s_addr != cur_entry->ipaddr_ia.s_addr || ( strncmp(cur_entry->ifname,dev,strlen(dev)) != 0 ) ) ) {
	prev_entry = cur_entry;
	cur_entry = cur_entry->next;
    };

    if (cur_entry == NULL) {
	if (debug) printf("Creating new arptab entry %s(%s)\n", inet_ntoa(ipaddr), dev);

	if ((cur_entry = (ARPTAB_ENTRY *) malloc(sizeof(ARPTAB_ENTRY))) == NULL) {
	    errstr = strerror(errno);
	    syslog(LOG_INFO, "No memory: %s", errstr);
	} else {
	    if (prev_entry == NULL) { *arptab=cur_entry; }
	    else { prev_entry->next = cur_entry; }
	    cur_entry->next = NULL;
	    cur_entry->ifname[0] = '\0';
	    cur_entry->route_added = 0;
	    cur_entry->want_route = 1;
	}
    }
    
    return cur_entry;	
}

int findentry(struct in_addr ipaddr)
{
    ARPTAB_ENTRY * cur_entry=*arptab;
    ARPTAB_ENTRY * prev_entry=NULL;
        
    while (cur_entry != NULL && ipaddr.s_addr != cur_entry->ipaddr_ia.s_addr) {
	prev_entry = cur_entry;
	cur_entry = cur_entry->next;
    };
    
    if (cur_entry == NULL)
	return 0;
    else
	return 1;
}

/* Remove all entires in arptab where ipaddr is NOT on interface dev */
int remove_other_routes(struct in_addr ipaddr, const char* dev)
{
    ARPTAB_ENTRY * cur_entry;
    int removed = 0;
        
    for (cur_entry=*arptab; cur_entry != NULL; cur_entry = cur_entry->next) {
        if (ipaddr.s_addr == cur_entry->ipaddr_ia.s_addr && strcmp(dev, cur_entry->ifname) != 0)  {
            if (debug && cur_entry->want_route) printf("Marking entry %s(%s) for removal\n", inet_ntoa(ipaddr), cur_entry->ifname);
            cur_entry->want_route = 0;
            ++removed;
        }
    }
    return removed;
}


/* Remove route from kernel */
int route_remove(ARPTAB_ENTRY* cur_entry)
{
    char routecmd_str[ROUTE_CMD_LEN];
    int success = 1;
    
    if (snprintf(routecmd_str, ROUTE_CMD_LEN-1, 
	    "/sbin/ip route del %s/32 metric 50 dev %s scope link",
	    inet_ntoa(cur_entry->ipaddr_ia), cur_entry->ifname) > ROUTE_CMD_LEN-1) 
    {
	syslog(LOG_INFO, "ip route command too large to fit in buffer!");
    } else {
	if (system(routecmd_str) != 0)
	{
	    syslog(LOG_INFO, "'%s' unsuccessful!", routecmd_str);
	    if (debug) printf("%s failed\n", routecmd_str);
	    success = 0;
	}
	else 
	{
	    if (debug) printf("%s success\n", routecmd_str);
	    success = 1;
	}
    }
    if (success)
	cur_entry->route_added = 0;
    
    return success;
}

/* Add route into kernel */
int route_add(ARPTAB_ENTRY* cur_entry)
{
    char routecmd_str[ROUTE_CMD_LEN];
    int success = 1;

    if (snprintf(routecmd_str, ROUTE_CMD_LEN-1, 
	    "/sbin/ip route add %s/32 metric 50 dev %s scope link",
	    inet_ntoa(cur_entry->ipaddr_ia), cur_entry->ifname) > ROUTE_CMD_LEN-1) 
    {
	syslog(LOG_INFO, "ip route command too large to fit in buffer!");
    } else {
	if (system(routecmd_str) != 0)
	{ 
	    syslog(LOG_INFO, "'%s' unsuccessful, will try to remove!", routecmd_str); 
	    if (debug) printf("%s failed\n", routecmd_str);
	    route_remove(cur_entry);
	    success = 0;
	}
	else
	{
	    if (debug) printf("%s success\n", routecmd_str);
	    success = 1;
	}
    }
    if (success)
	cur_entry->route_added = 1;

    return success;
}


void processarp(int in_cleanup) 
{
    ARPTAB_ENTRY *cur_entry=*arptab, *prev_entry=NULL;

    /* First loop to remove unwanted routes */
    while (cur_entry != NULL) {
	if (debug && verbose) printf("Working on route %s(%s) tstamp %u want_route %d\n", inet_ntoa(cur_entry->ipaddr_ia), cur_entry->ifname, (int) cur_entry->tstamp, cur_entry->want_route);

	if ( !cur_entry->want_route
	    || time(NULL) - cur_entry->tstamp > ARP_TABLE_ENTRY_TIMEOUT 
	    || in_cleanup)  {
	    
	    if (cur_entry->route_added)
		route_remove(cur_entry);

	    /* remove from arp list */
	    if (debug) printf("Delete arp %s(%s)\n", inet_ntoa(cur_entry->ipaddr_ia), cur_entry->ifname);
		
	    if (prev_entry != NULL) {
	        prev_entry->next = cur_entry->next;
	        free(cur_entry);
	        cur_entry=prev_entry->next;
	    } else {
	        *arptab = cur_entry->next;
	        free(cur_entry);
        	cur_entry=*arptab;
            }
	} else {
    	    prev_entry = cur_entry;
	    cur_entry = cur_entry->next;
	}	
    } /* while loop */

    /* Now loop to add new routes */
    cur_entry=*arptab;
    while (cur_entry != NULL) {
	if (time(NULL) - cur_entry->tstamp <= ARP_TABLE_ENTRY_TIMEOUT 
		&& cur_entry->want_route
		&& !cur_entry->route_added 
		&& !in_cleanup) 
	{
	    /* add route to the kernel */
	    route_add(cur_entry);
	}
	cur_entry = cur_entry->next;
    } /* while loop */

}	

void parseproc()
{
    FILE *arpf;
    int firstline;
    ARPTAB_ENTRY *entry;
    char line[ARP_LINE_LEN];
    struct in_addr ipaddr;
    int incomplete=0;
    int i;
    char *ip, *mac, *dev, *hw, *flags, *mask;
    
    /* Parse /proc/net/arp table */
        
    if ((arpf = fopen(PROC_ARP, "r")) == NULL) {
	errstr = strerror(errno);
	syslog(LOG_INFO, "Error during ARP table open: %s", errstr);
    }

    firstline=1;

    while (!feof(arpf)) {
	
	if (fgets(line, ARP_LINE_LEN, arpf) == NULL) {
	    if (!ferror(arpf))
		break;
	    else {
    		errstr = strerror(errno);
		syslog(LOG_INFO, "Error during ARP table open: %s", errstr);
	    }
	} else {
	    if (firstline) { firstline=0; continue; }
	    if (debug && verbose) printf("read ARP line %s", line);

	    incomplete=0;
	    	    
	    /* Incomplete ARP entries with MAC 00:00:00:00:00:00 */
	    if (strstr(line, "00:00:00:00:00:00") != NULL) 
		incomplete=1;
		
	    /* Incomplete entries having flag 0x0 */
	    if (strstr(line, "0x0") != NULL)
		incomplete=1;
	    
	    ip=strtok(line, " ");

	    if ((inet_aton(ip, &ipaddr)) == -1)
		    syslog(LOG_INFO, "Error parsing IP address %s", ip);
			
	    /* if IP address is marked as undiscovered and does not exist in arptab,
	       send ARP request to all ifaces */

	    if (incomplete &! findentry(ipaddr) ) {
	    	if (debug)  printf("incomplete entry %s found, request on all interfaces\n", inet_ntoa(ipaddr));
		for (i=0; i <= last_iface_idx; i++)
		    arp_req(ifaces[i], ipaddr, 0);
	    }

	    /* Hardware type */
	    hw=strtok(NULL, " "); 
	    
	    /* flags */
	    flags=strtok(NULL, " "); 

	    /* MAC address */	    
	    mac=strtok(NULL, " ");

	    /* Mask */
	    mask=strtok(NULL, " "); 

	    /* Device */
	    dev=strtok(NULL, " ");

	    if (dev[strlen(dev)-1] == '\n') { dev[strlen(dev)-1] = '\0'; }

	    entry=replace_entry(ipaddr, dev);
	    
	    if (entry->incomplete != incomplete && debug)
	    	printf("change entry %s(%s) to incomplete=%d\n", ip, dev, incomplete);

	    entry->ipaddr_ia.s_addr = ipaddr.s_addr;
	    entry->incomplete       = incomplete;
	    
	    if (strlen(mac) < ARP_TABLE_ENTRY_LEN)
		strncpy(entry->hwaddr, mac, ARP_TABLE_ENTRY_LEN);
	    else 
		syslog(LOG_INFO, "Error during ARP table parsing");
	    
	    if (strlen(dev) < ARP_TABLE_ENTRY_LEN) 
		strncpy(entry->ifname, dev, ARP_TABLE_ENTRY_LEN);
	    else
		syslog(LOG_INFO, "Error during ARP table parsing");

	    /* do not add routes for incomplete entries */
	    if (debug && entry->want_route != !incomplete) 
		printf("%s(%s): set want_route %d\n", inet_ntoa(entry->ipaddr_ia), entry->ifname, !incomplete);
	    entry->want_route   = !incomplete; 

            /* Remove route from kernel if it already exists through
               a different interface */
            if (entry->want_route)
            {
                if (remove_other_routes(entry->ipaddr_ia, entry->ifname) > 0)
                    if (debug) printf("Found ARP entry %s(%s), removed entries via other interfaces\n", inet_ntoa(entry->ipaddr_ia), entry->ifname);
            }

	    time(&entry->tstamp);
	    
	    if (debug && !entry->route_added && entry->want_route) {
	        printf("arptab entry: '%s' HWAddr: '%s' Dev: '%s' route_added:%d want_route:%d\n", 
		    inet_ntoa(entry->ipaddr_ia), entry->hwaddr, entry->ifname, entry->route_added, entry->want_route);
	    }
	}
    }

    if (fclose(arpf)) {
	errstr = strerror(errno);
	syslog(LOG_INFO, "Error during ARP table open: %s", errstr);
    }
}

void cleanup() 
{
    /* FIXME: I think this is a wrong way to do it ... */
    
    syslog(LOG_INFO, "Received signal; cleaning up.");
    /*
    for (i=0; i <= last_thread_idx; i++) {
	pthread_cancel(my_threads[i]);
    }
    */
    pthread_mutex_trylock(&arptab_mutex);
    processarp(1);
    syslog(LOG_INFO, "Terminating.");
    exit(1);
}

void sighandler()
{
    /* FIXME: I think this is a wrong way to do it ... */
    perform_shutdown=1;
}

void *main_thread()
{
    time_t last_refresh;

    signal(SIGINT, sighandler);
    signal(SIGTERM, sighandler);
    signal(SIGHUP, sighandler);
    
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
    pthread_cleanup_push(cleanup, NULL);
    while (1) {
	if (perform_shutdown) {
	    pthread_exit(0);
	}
	pthread_testcancel();
        pthread_mutex_lock(&arptab_mutex);
        parseproc();
        processarp(0);
	pthread_mutex_unlock(&arptab_mutex);
	usleep(SLEEPTIME);
	if (!option_arpperm && time(NULL)-last_refresh > REFRESHTIME) {
	    pthread_mutex_lock(&arptab_mutex);
	    refresharp(*arptab);
	    pthread_mutex_unlock(&arptab_mutex);
	    time(&last_refresh);
	}
    }
    /* required since pthread_cleanup_* are implemented as macros */
    pthread_cleanup_pop(0);
}
    
int main (int argc, char **argv)
{
    pid_t child_pid;
    int i, help=1;
    
    progname = (char *) basename(argv[0]);
    
    for (i = 1; i < argc; i++) {
	if (!strcmp(argv[i],"-d")) { 
	    debug=1;
	    help=0;
	}
	else if (!strcmp(argv[i],"-p")) { 
	    option_arpperm=1;
	    help=0;
	}
	else if (!strcmp(argv[i],"-h") || !strcmp(argv[i],"--help")) {
	    break;
	}
	else {
	    last_iface_idx++;
	    ifaces[last_iface_idx]=argv[i];
	    help=0;
	}
    }

    if (help || last_iface_idx <= -1) {
	    printf("parprouted: proxy ARP routing daemon, version %s.\n", VERSION);
    	    printf("(C) 2007 Vladimir Ivaschenko <vi@maks.net>, GPL2 license.\n");
	    printf("Usage: parprouted [-d] [-p] interface [interface]\n");
	    exit(1);
    }

    if (!debug) {
        /* fork to go into the background */
        if ((child_pid = fork()) < 0) {
            fprintf(stderr, "could not fork(): %s", strerror(errno));
            exit(1);
        } else if (child_pid > 0) {
            /* fork was ok, wait for child to exit */
            if (waitpid(child_pid, NULL, 0) != child_pid) {
                perror(progname);
                exit(1);
            }
            /* and exit myself */
            exit(0);
        }
        /* and fork again to make sure we inherit all rights from init */
        if ((child_pid = fork()) < 0) {
            perror(progname);
            exit(1);
        } else if (child_pid > 0)
            exit(0);

        /* create our own session */
        setsid();

        /* close stdin/stdout/stderr */
        close(0);
        close(1);
        close(2);

    }

    openlog(progname, LOG_PID | LOG_CONS | LOG_PERROR, LOG_DAEMON);
    syslog(LOG_INFO, "Starting.");

    signal(SIGINT, sighandler);
    signal(SIGTERM, sighandler);
    signal(SIGHUP, sighandler);

    if ((arptab = (ARPTAB_ENTRY **) malloc(sizeof(ARPTAB_ENTRY **))) == NULL) {
	    errstr = strerror(errno);
	    syslog(LOG_INFO, "No memory: %s", errstr);
    }
    
    *arptab = NULL;

    pthread_mutex_init(&arptab_mutex, NULL);
    pthread_mutex_init(&req_queue_mutex, NULL);
    
    if (pthread_create(&my_threads[++last_thread_idx], NULL, main_thread, NULL)) {
	syslog(LOG_ERR, "Error creating main thread.");
	abort();
    }

    for (i=0; i <= last_iface_idx; i++) {
	if (pthread_create(&my_threads[++last_thread_idx], NULL, (void *) arp, (void *) ifaces[i])) {
	    syslog(LOG_ERR, "Error creating ARP thread for %s.",ifaces[i]);
	    abort();
	}
	if (debug) printf("Created ARP thread for %s.\n",ifaces[i]);
    }
        
    if (pthread_join(my_threads[0], NULL)) {
	syslog(LOG_ERR, "Error joining thread.");
	abort();
    }

    while (waitpid(-1, NULL, WNOHANG)) { }
    exit(1);     
}
