
/*
 * (C)Copyright (C) 2012 by Darren Reed.
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/ioctl.h>

#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>

#include <net/if.h>

#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>

#include "ip_compat.h"
#include "ip_fil.h"
#include "ip_nat.h"

#include "ipf.h"

extern	char	*optarg;


typedef	struct	l4cfg	{
	struct	l4cfg		*l4_next;
	struct	ipnat		l4_nat;		/* NAT rule */
	struct	sockaddr_in	l4_sin;		/* remote socket to connect */
	time_t			l4_last;	/* when we last connected */
	int			l4_alive;	/* 1 = remote alive */
	int			l4_fd;
	int			l4_rw;		/* 0 = reading, 1 = writing */
	char			*l4_rbuf;	/* read buffer */
	int			l4_rsize;	/* size of buffer */
	int			l4_rlen;	/* how much used */
	char			*l4_wptr;	/* next byte to write */
	int			l4_wlen;	/* length yet to be written */
} l4cfg_t;


l4cfg_t *l4list = NULL;
char *response = NULL;
char *probe = NULL;
l4cfg_t template;
int frequency = 20;
int ctimeout = 1;
int rtimeout = 1;
size_t plen = 0;
size_t rlen = 0;
int natfd = -1;
int opts = 0;

#if defined(sun) && !defined(__svr4__) && !defined(__SVR4)
# define	strerror(x)	sys_errlist[x]
#endif


char *
copystr(char *dst, char *src)
{
	register char *s, *t, c;
	register int esc = 0;

	for (s = src, t = dst; s && t && (c = *s++); )
		if (esc) {
			esc = 0;
			switch (c)
			{
			case 'n' :
				*t++ = '\n';
				break;
			case 'r' :
				*t++ = '\r';
				break;
			case 't' :
				*t++ = '\t';
				break;
			}
		} else if (c != '\\')
			*t++ = c;
		else
			esc = 1;
	*t = '\0';
	return(dst);
}

void
addnat(l4cfg_t *l4)
{
	ipnat_t *ipn = &l4->l4_nat;

	printf("Add NAT rule for %s/%#x,%u -> ", inet_ntoa(ipn->in_out[0]),
		ipn->in_outmsk, ntohs(ipn->in_pmin));
	printf("%s,%u\n", inet_ntoa(ipn->in_in[0]), ntohs(ipn->in_pnext));
	if (!(opts & OPT_DONOTHING)) {
		if (ioctl(natfd, SIOCADNAT, &ipn) == -1)
			perror("ioctl(SIOCADNAT)");
	}
}


void
delnat(l4cfg_t *l4)
{
	ipnat_t *ipn = &l4->l4_nat;

	printf("Remove NAT rule for %s/%#x,%u -> ",
		inet_ntoa(ipn->in_out[0]), ipn->in_outmsk, ipn->in_pmin);
	printf("%s,%u\n", inet_ntoa(ipn->in_in[0]), ipn->in_pnext);
	if (!(opts & OPT_DONOTHING)) {
		if (ioctl(natfd, SIOCRMNAT, &ipn) == -1)
			perror("ioctl(SIOCRMNAT)");
	}
}


void
connectl4(l4cfg_t *l4)
{
	l4->l4_rw = 1;
	l4->l4_rlen = 0;
	l4->l4_wlen = plen;
	if (!l4->l4_wlen) {
		l4->l4_alive = 1;
		addnat(l4);
	} else
		l4->l4_wptr = probe;
}


void
closel4(l4cfg_t *l4, int dead)
{
	close(l4->l4_fd);
	l4->l4_fd = -1;
	l4->l4_rw = -1;
	if (dead && l4->l4_alive) {
		l4->l4_alive = 0;
		delnat(l4);
	}
}


void
connectfd(l4cfg_t *l4)
{
	if (connect(l4->l4_fd, (struct sockaddr *)&l4->l4_sin,
		    sizeof(l4->l4_sin)) == -1) {
		if (errno == EISCONN) {
			if (opts & OPT_VERBOSE)
				fprintf(stderr, "Connected fd %d\n",
					l4->l4_fd);
			connectl4(l4);
			return;
		}
		if (opts & OPT_VERBOSE)
			fprintf(stderr, "Connect failed fd %d: %s\n",
				l4->l4_fd, strerror(errno));
		closel4(l4, 1);
		return;
	}
	l4->l4_rw = 1;
}


void
writefd(l4cfg_t *l4)
{
	char buf[80], *ptr;
	int n, i, fd;

	fd = l4->l4_fd;

	if (l4->l4_rw == -2) {
		connectfd(l4);
		return;
	}

	n = l4->l4_wlen;

	i = send(fd, l4->l4_wptr, n, 0);
	if (i == 0 || i == -1) {
		if (opts & OPT_VERBOSE)
			fprintf(stderr, "Send on fd %d failed: %s\n",
				fd, strerror(errno));
		closel4(l4, 1);
	} else {
		l4->l4_wptr += i;
		l4->l4_wlen -= i;
		if (l4->l4_wlen == 0)
			l4->l4_rw = 0;
		if (opts & OPT_VERBOSE)
			fprintf(stderr, "Sent %d bytes to fd %d\n", i, fd);
	}
}


void readfd(l4cfg_t *l4)
{
	char buf[80], *ptr;
	int n, i, fd;

	fd = l4->l4_fd;

	if (l4->l4_rw == -2) {
		connectfd(l4);
		return;
	}

	if (l4->l4_rsize) {
		n = l4->l4_rsize - l4->l4_rlen;
		ptr = l4->l4_rbuf + l4->l4_rlen;
	} else {
		n = sizeof(buf) - 1;
		ptr = buf;
	}

	if (opts & OPT_VERBOSE)
		fprintf(stderr, "Read %d bytes on fd %d to %p\n",
			n, fd, ptr);
	i = recv(fd, ptr, n, 0);
	if (i == 0 || i == -1) {
		if (opts & OPT_VERBOSE)
			fprintf(stderr, "Read error on fd %d: %s\n",
				fd, (i == 0) ? "EOF" : strerror(errno));
		closel4(l4, 1);
	} else {
		if (ptr == buf)
			ptr[i] = '\0';
		if (opts & OPT_VERBOSE)
			fprintf(stderr, "%d: Read %d bytes [%*.*s]\n",
				fd, i, i, i, ptr);
		if (ptr != buf) {
			l4->l4_rlen += i;
			if (l4->l4_rlen >= l4->l4_rsize) {
				if (!strncmp(response, l4->l4_rbuf,
					     l4->l4_rsize)) {
					printf("%d: Good response\n",
						fd);
					if (!l4->l4_alive) {
						l4->l4_alive = 1;
						addnat(l4);
					}
					closel4(l4, 0);
				} else {
					if (opts & OPT_VERBOSE)
						printf("%d: Bad response\n",
							fd);
					closel4(l4, 1);
				}
			}
		} else if (!l4->l4_alive) {
			l4->l4_alive = 1;
			addnat(l4);
			closel4(l4, 0);
		}
	}
}


int
runconfig(void)
{
	int fd, opt, res, mfd, i;
	struct timeval tv;
	time_t now, now1;
	fd_set rfd, wfd;
	l4cfg_t *l4;

	mfd = 0;
	opt = 1;
	now = time(NULL);

	/*
	 * First, initiate connections that are closed, as required.
	 */
	for (l4 = l4list; l4; l4 = l4->l4_next) {
		if ((l4->l4_last + frequency < now) && (l4->l4_fd == -1)) {
			l4->l4_last = now;
			fd = socket(AF_INET, SOCK_STREAM, 0);
			if (fd == -1)
				continue;
			setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt,
				   sizeof(opt));
#ifdef	O_NONBLOCK
			if ((res = fcntl(fd, F_GETFL, 0)) != -1)
				fcntl(fd, F_SETFL, res | O_NONBLOCK);
#endif
			if (opts & OPT_VERBOSE)
				fprintf(stderr,
					"Connecting to %s,%d (fd %d)...",
					inet_ntoa(l4->l4_sin.sin_addr),
					ntohs(l4->l4_sin.sin_port), fd);
			if (connect(fd, (struct sockaddr *)&l4->l4_sin,
				    sizeof(l4->l4_sin)) == -1) {
				if (errno != EINPROGRESS) {
					if (opts & OPT_VERBOSE)
						fprintf(stderr, "failed\n");
					perror("connect");
					close(fd);
					fd = -1;
				} else {
					if (opts & OPT_VERBOSE)
						fprintf(stderr, "waiting\n");
					l4->l4_rw = -2;
				}
			} else {
				if (opts & OPT_VERBOSE)
					fprintf(stderr, "connected\n");
				connectl4(l4);
			}
			l4->l4_fd = fd;
		}
	}

	/*
	 * Now look for fd's which we're expecting to read/write from.
	 */
	FD_ZERO(&rfd);
	FD_ZERO(&wfd);
	tv.tv_sec = MIN(rtimeout, ctimeout);
	tv.tv_usec = 0;

	for (l4 = l4list; l4; l4 = l4->l4_next)
		if (l4->l4_rw == 0) {
			if (now - l4->l4_last > rtimeout) {
				if (opts & OPT_VERBOSE)
					fprintf(stderr, "%d: Read timeout\n",
						l4->l4_fd);
				closel4(l4, 1);
				continue;
			}
			if (opts & OPT_VERBOSE)
				fprintf(stderr, "Wait for read on fd %d\n",
					l4->l4_fd);
			FD_SET(l4->l4_fd, &rfd);
			if (l4->l4_fd > mfd)
				mfd = l4->l4_fd;
		} else if ((l4->l4_rw == 1 && l4->l4_wlen) ||
			   l4->l4_rw == -2) {
			if ((l4->l4_rw == -2) &&
			    (now - l4->l4_last > ctimeout)) {
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"%d: connect timeout\n",
						l4->l4_fd);
				closel4(l4);
				continue;
			}
			if (opts & OPT_VERBOSE)
				fprintf(stderr, "Wait for write on fd %d\n",
					l4->l4_fd);
			FD_SET(l4->l4_fd, &wfd);
			if (l4->l4_fd > mfd)
				mfd = l4->l4_fd;
		}

	if (opts & OPT_VERBOSE)
		fprintf(stderr, "Select: max fd %d wait %d\n", mfd + 1,
			tv.tv_sec);
	i = select(mfd + 1, &rfd, &wfd, NULL, &tv);
	if (i == -1) {
		perror("select");
		return(-1);
	}

	now1 = time(NULL);

	for (l4 = l4list; (i > 0) && l4; l4 = l4->l4_next) {
		if (l4->l4_fd < 0)
			continue;
		if (FD_ISSET(l4->l4_fd, &rfd)) {
			if (opts & OPT_VERBOSE)
				fprintf(stderr, "Ready to read on fd %d\n",
					l4->l4_fd);
			readfd(l4);
			i--;
		}

		if ((l4->l4_fd >= 0) && FD_ISSET(l4->l4_fd, &wfd)) {
			if (opts & OPT_VERBOSE)
				fprintf(stderr, "Ready to write on fd %d\n",
					l4->l4_fd);
			writefd(l4);
			i--;
		}
	}
	return(0);
}


int
gethostport(char *str, int lnum, u_32_t *ipp, u_short *portp)
{
	struct servent *sp;
	struct hostent *hp;
	char *host, *port;
	struct in_addr ip;

	host = str;
	port = strchr(host, ',');
	if (port)
		*port++ = '\0';

#ifdef	HAVE_INET_ATON
	if (ISDIGIT(*host) && inet_aton(host, &ip))
		*ipp = ip.s_addr;
#else
	if (ISDIGIT(*host))
		*ipp = inet_addr(host);
#endif
	else {
		if (!(hp = gethostbyname(host))) {
			fprintf(stderr, "%d: can't resolve hostname: %s\n",
				lnum, host);
			return(0);
		}
		*ipp = *(u_32_t *)hp->h_addr;
	}

	if (port) {
		if (ISDIGIT(*port))
			*portp = htons(atoi(port));
		else {
			sp = getservbyname(port, "tcp");
			if (sp)
				*portp = sp->s_port;
			else {
				fprintf(stderr, "%d: unknown service %s\n",
					lnum, port);
				return(0);
			}
		}
	} else
		*portp = 0;
	return(1);
}


char *
mapfile(char *file, size_t *sizep)
{
	struct stat sb;
	caddr_t addr;
	int fd;

	fd = open(file, O_RDONLY);
	if (fd == -1) {
		perror("open(mapfile)");
		return(NULL);
	}

	if (fstat(fd, &sb) == -1) {
		perror("fstat(mapfile)");
		close(fd);
		return(NULL);
	}

	addr = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
	if (addr == (caddr_t)-1) {
		perror("mmap(mapfile)");
		close(fd);
		return(NULL);
	}
	close(fd);
	*sizep = sb.st_size;
	return(char *)addr;
}


int
readconfig(char *filename)
{
	char c, buf[512], *s, *t, *errtxt = NULL, *line;
	int num, err = 0;
	ipnat_t *ipn;
	l4cfg_t *l4;
	FILE *fp;

	fp = fopen(filename, "r");
	if (!fp) {
		perror("open(configfile)");
		return(-1);
	}

	bzero((char *)&template, sizeof(template));
	template.l4_fd = -1;
	template.l4_rw = -1;
	template.l4_sin.sin_family = AF_INET;
	ipn = &template.l4_nat;
	ipn->in_flags = IPN_TCP|IPN_ROUNDR;
	ipn->in_redir = NAT_REDIRECT;

	for (num = 1; fgets(buf, sizeof(buf), fp); num++) {
		s = strchr(buf, '\n');
		if  (!s) {
			fprintf(stderr, "%d: line too long\n", num);
			fclose(fp);
			return(-1);
		}

		*s = '\0';

		/*
		 * lines which are comments
		 */
		s = strchr(buf, '#');
		if (s)
			*s = '\0';

		/*
		 * Skip leading whitespace
		 */
		for (line = buf; (c = *line) && ISSPACE(c); line++)
			;
		if (!*line)
			continue;

		if (opts & OPT_VERBOSE)
			fprintf(stderr, "Parsing: [%s]\n", line);
		t = strtok(line, " \t");
		if (!t)
			continue;
		if (!strcasecmp(t, "interface")) {
			s = strtok(NULL, " \t");
			if (s)
				t = strtok(NULL, "\t");
			if (!s || !t) {
				errtxt = line;
				err = -1;
				break;
			}

			if (!strchr(t, ',')) {
				fprintf(stderr,
					"%d: local address,port missing\n",
					num);
				err = -1;
				break;
			}

			strncpy(ipn->in_ifname, s, sizeof(ipn->in_ifname));
			if (!gethostport(t, num, &ipn->in_outip,
					 &ipn->in_pmin)) {
				errtxt = line;
				err = -1;
				break;
			}
			ipn->in_outmsk = 0xffffffff;
			ipn->in_pmax = ipn->in_pmin;
			if (opts & OPT_VERBOSE)
				fprintf(stderr,
					"Interface %s %s/%#x port %u\n",
					ipn->in_ifname,
					inet_ntoa(ipn->in_out[0]),
					ipn->in_outmsk, ipn->in_pmin);
		} else if (!strcasecmp(t, "remote")) {
			if (!*ipn->in_ifname) {
				fprintf(stderr,
					"%d: ifname not set prior to remote\n",
					num);
				err = -1;
				break;
			}
			s = strtok(NULL, " \t");
			if (s)
				t = strtok(NULL, "");
			if (!s || !t || strcasecmp(s, "server")) {
				errtxt = line;
				err = -1;
				break;
			}

			ipn->in_pnext = 0;
			if (!gethostport(t, num, &ipn->in_inip,
					 &ipn->in_pnext)) {
				errtxt = line;
				err = -1;
				break;
			}
			ipn->in_inmsk = 0xffffffff;
			if (ipn->in_pnext == 0)
				ipn->in_pnext = ipn->in_pmin;

			l4 = (l4cfg_t *)malloc(sizeof(*l4));
			if (!l4) {
				fprintf(stderr, "%d: out of memory (%d)\n",
					num, sizeof(*l4));
				err = -1;
				break;
			}
			bcopy((char *)&template, (char *)l4, sizeof(*l4));
			l4->l4_sin.sin_addr = ipn->in_in[0];
			l4->l4_sin.sin_port = ipn->in_pnext;
			l4->l4_next = l4list;
			l4list = l4;
		} else if (!strcasecmp(t, "connect")) {
			s = strtok(NULL, " \t");
			if (s)
				t = strtok(NULL, "\t");
			if (!s || !t) {
				errtxt = line;
				err = -1;
				break;
			} else if (!strcasecmp(s, "timeout")) {
				ctimeout = atoi(t);
				if (opts & OPT_VERBOSE)
					fprintf(stderr, "connect timeout %d\n",
						ctimeout);
			} else if (!strcasecmp(s, "frequency")) {
				frequency = atoi(t);
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"connect frequency %d\n",
						frequency);
			} else {
				errtxt = line;
				err = -1;
				break;
			}
		} else if (!strcasecmp(t, "probe")) {
			s = strtok(NULL, " \t");
			if (!s) {
				errtxt = line;
				err = -1;
				break;
			} else if (!strcasecmp(s, "string")) {
				if (probe) {
					fprintf(stderr,
						"%d: probe already set\n",
						num);
					err = -1;
					break;
				}
				t = strtok(NULL, "");
				if (!t) {
					fprintf(stderr,
						"%d: No probe string\n", num);
					err = -1;
					break;
				}

				probe = malloc(strlen(t));
				copystr(probe, t);
				plen = strlen(probe);
				if (opts & OPT_VERBOSE)
					fprintf(stderr, "Probe string [%s]\n",
						probe);
			} else if (!strcasecmp(s, "file")) {
				t = strtok(NULL, " \t");
				if (!t) {
					errtxt = line;
					err = -1;
					break;
				}
				if (probe) {
					fprintf(stderr,
						"%d: probe already set\n",
						num);
					err = -1;
					break;
				}
				probe = mapfile(t, &plen);
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"Probe file %s len %u@%p\n",
						t, plen, probe);
			}
		} else if (!strcasecmp(t, "response")) {
			s = strtok(NULL, " \t");
			if (!s) {
				errtxt = line;
				err = -1;
				break;
			} else if (!strcasecmp(s, "timeout")) {
				t = strtok(NULL, " \t");
				if (!t) {
					errtxt = line;
					err = -1;
					break;
				}
				rtimeout = atoi(t);
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"response timeout %d\n",
						rtimeout);
			} else if (!strcasecmp(s, "string")) {
				if (response) {
					fprintf(stderr,
						"%d: response already set\n",
						num);
					err = -1;
					break;
				}
				response = strdup(strtok(NULL, ""));
				rlen = strlen(response);
				template.l4_rsize = rlen;
				template.l4_rbuf = malloc(rlen);
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"Response string [%s]\n",
						response);
			} else if (!strcasecmp(s, "file")) {
				t = strtok(NULL, " \t");
				if (!t) {
					errtxt = line;
					err = -1;
					break;
				}
				if (response) {
					fprintf(stderr,
						"%d: response already set\n",
						num);
					err = -1;
					break;
				}
				response = mapfile(t, &rlen);
				template.l4_rsize = rlen;
				template.l4_rbuf = malloc(rlen);
				if (opts & OPT_VERBOSE)
					fprintf(stderr,
						"Response file %s len %u@%p\n",
						t, rlen, response);
			}
		} else {
			errtxt = line;
			err = -1;
			break;
		}
	}

	if (errtxt)
		fprintf(stderr, "%d: syntax error at \"%s\"\n", num, errtxt);
	fclose(fp);
	return(err);
}


void
usage(char *prog)
{
	fprintf(stderr, "Usage: %s -f <configfile>\n", prog);
	exit(1);
}


int
main(int argc, char *argv[])
{
	char *config = NULL;
	int c;

	while ((c = getopt(argc, argv, "f:nv")) != -1)
		switch (c)
		{
		case 'f' :
			config = optarg;
			break;
		case 'n' :
			opts |= OPT_DONOTHING;
			break;
		case 'v' :
			opts |= OPT_VERBOSE;
			break;
		}

	if (config == NULL)
		usage(argv[0]);

	if (readconfig(config))
		exit(1);

	if (!l4list) {
		fprintf(stderr, "No remote servers, exiting.");
		exit(1);
	}

	if (!(opts & OPT_DONOTHING)) {
		natfd = open(IPL_NAT, O_RDWR);
		if (natfd == -1) {
			perror("open(IPL_NAT)");
			exit(1);
		}
	}

	if (opts & OPT_VERBOSE)
		fprintf(stderr, "Starting...\n");
	while (runconfig() == 0)
		;
}
