/*
    ipx_cmd.c - IPX Compatibility mode tunnel
    Copyright (C) 1999  Petr Vandrovec

    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


    Revision history:

	0.00  1999			Petr Vandrovec <vandrove@vc.cvut.cz>
		Initial revision.

	1.00  1999, November 20		Petr Vandrovec <vandrove@vc.cvut.cz>
		Added license.

	1.01  2001, June 16		Petr Vandrovec <vandrove@vc.cvut.cz>
		Cleanup some compilation warnings.

 */
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <ncp/ext/socket.h>
#include <sys/ioctl.h>
#include <netdb.h>
#include <net/if_arp.h>	/* TODO: linux/if_arp.h */
#include <ncp/kernel/ipx.h>
#include <ncp/kernel/if.h>
#include <ncp/kernel/types.h>
#include "netlink.h"

#include "private/libintl.h"
#define _(X) gettext(X)

#define NOVELL_SCMD_PORT	(2645)

/* Move them to configure... */
#ifndef AF_NETLINK
#define AF_NETLINK 16
#endif
#ifndef PF_NETLINK
#define PF_NETLINK AF_NETLINK
#endif

/* we are doing EthernetII... Any objections? */
struct {
	u_int16_t	unknown	__attribute__((packed));
	u_int8_t	dst[6]	__attribute__((packed));
	u_int8_t	src[6]	__attribute__((packed));
	u_int16_t	type	__attribute__((packed));
	u_int8_t	ipx[16384];
	} buffer;

static int getiflist(int fd, struct ifconf* ifc) {
	int i;
	
	for (i = 0; i < 32; i++) {
		int err;
		
		ifc->ifc_len = 65536;	/* ignored */
		ifc->ifc_req = NULL;
		
		err = ioctl(fd, SIOCGIFCONF, ifc);
		if (err < 0)
			continue;
			
		if (!ifc->ifc_len)
			return 0;
			
		ifc->ifc_req = (struct ifreq*)malloc(ifc->ifc_len);
		err = ioctl(fd, SIOCGIFCONF, ifc);
		if (!err)
			return 0;
		free(ifc->ifc_buf);
	}
	ifc->ifc_len = 0;
	ifc->ifc_req = NULL;
	return errno;		
}

static char* progname;

static int findtap(int fd_ipx, u_int32_t local) {
	int i;
	u_int8_t localhw[6];
	
	localhw[0] = 0x7E;
	localhw[1] = 0x01;
	memcpy(localhw+2, &local, 4);
	
	for (i = 0; i < 16; i++) {
		struct ifreq ifr;
		int err;
		
		sprintf(ifr.ifr_name, "tap%d", i);
		((struct sockaddr_ipx*)&ifr.ifr_addr)->sipx_type = IPX_FRAME_ETHERII;
		err = ioctl(fd_ipx, SIOCGIFHWADDR, &ifr);
		if (err >= 0) {
			if ((ifr.ifr_addr.sa_family == ARPHRD_ETHER) && !memcmp(ifr.ifr_addr.sa_data, localhw, 6)) {
				err = ioctl(fd_ipx, SIOCGIFADDR, &ifr);
				if (err >= 0) {
					if (!memcmp(((struct sockaddr_ipx*)&ifr.ifr_addr)->sipx_node, localhw, 6)) {
						return i;
					}
				}
			}
		}
	}
	return -EADDRNOTAVAIL;
}
	
static void usage(void) {
	fprintf(stderr, _("usage: %s -A migration_agent [-l local_ip]\n"), progname);
	return;
}

int main(int argc, char* argv[]) {
	int fd_ip;
	struct sockaddr_in localIP;
	int status;
	int fd_netlink;
	struct sockaddr_nl localNET;
	int arg;
	int maxfd;
	fd_set rcvset;
	int opt;
	char *server = NULL;
	struct hostent* hent;
	u_int32_t serveraddr;
	u_int32_t local = htonl(INADDR_ANY);
	int local_ok;
	struct ifconf ifc;
	struct ifreq* curr;
	struct ifreq* stop;
	int tapnum;
	int fd_ipx;
	
	setlocale(LC_ALL, "");
	bindtextdomain(NCPFS_PACKAGE, LOCALEDIR);
	textdomain(NCPFS_PACKAGE);
	
	{
		char* i;
		
		i = strrchr(argv[0], '/');
		progname = i?i+1:argv[0];
	}	
	
	while ((opt = getopt(argc, argv, "A:l:")) != -1) {
		switch (opt) {
			case 'A':
				server = optarg;
				break;
			case 'l':
				{
					hent = gethostbyname(optarg);
					if (!hent) {
						fprintf(stderr, _("%s: %s: "),
							progname,
							optarg);
						herror("");
						exit(99);
					}
					if (hent->h_addrtype != AF_INET) {
						fprintf(stderr, _("%s: Address of %s is not IP\n"),
							progname,
							optarg);
						exit(99);
					}
					if (hent->h_length != 4) {
						fprintf(stderr, _("%s: Address of %s is not 4 bytes long\n"),
							progname,
							optarg);
						exit(99);
					}
					memcpy(&local, hent->h_addr, 4);
				}
				break;
			default:
				usage();
				exit(99);
		}
	}
	
	if (!server) {
		usage();
		exit(99);
	}
	
	hent = gethostbyname(server);
	if (!hent) {
		fprintf(stderr, _("%s: %s: "),
			progname,
			server);
		herror("");
		exit(99);
	}
	if (hent->h_addrtype != AF_INET) {
		fprintf(stderr, _("%s: Address of %s is not IP\n"),
			progname,
			server);
		exit(99);
	}
	if (hent->h_length != 4) {
		fprintf(stderr, _("%s: Address of %s is not 4 bytes long\n"),
			progname,
			server);
		exit(99);
	}
	memcpy(&serveraddr, hent->h_addr, 4);

	fd_ip = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (fd_ip == -1) {
		int err = errno;
		fprintf(stderr, _("%s: Cannot create UDP/IP socket: %s\n"),
			progname,
			strerror(err));
		return 1;
	}
	
	status = getiflist(fd_ip, &ifc);
	if (status) {
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot get list of local interfaces: %s\n"),
			progname,
			strerror(status));
		return 1;
	}
	curr = ifc.ifc_req;
	stop = (struct ifreq*)(((u_int8_t*)curr) + ifc.ifc_len);

	local_ok = 0;
	while (curr < stop) {
		if (curr->ifr_addr.sa_family == AF_INET) {
			u_int32_t net = ((struct sockaddr_in*)&curr->ifr_addr)->sin_addr.s_addr;
			if (((ntohl(net) & IN_CLASSA_NET) >> IN_CLASSA_NSHIFT) != IN_LOOPBACKNET) {
				if (local == htonl(INADDR_ANY))
					local = net;
				if (local == net)
					local_ok = 1;
			}
		}
		curr++;
	}
	free(ifc.ifc_req);
	if (!local_ok) {
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot find local requested address\n"),
			progname);
		return 1;
	}
	
	/* TBD: enable SO_REUSEADDR? */
	localIP.sin_family = AF_INET;
	localIP.sin_addr.s_addr = htonl(INADDR_ANY);
	localIP.sin_port = htons(NOVELL_SCMD_PORT);
	status = bind(fd_ip, (struct sockaddr*)&localIP, sizeof(localIP));
	if (status) {
		int err = errno;
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot bind requested address to IP socket: %s\n"),
			progname,
			strerror(err));
		return 1;
	}

	/* OK, we have local IP address... Now find appropriate tap device */
	fd_ipx = socket(PF_IPX, SOCK_DGRAM, IPXPROTO_IPX);
	if (fd_ipx < 0) {
		int err = errno;
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot create IPX socket: %s\n"),
			progname,
			strerror(err));
		return 1;
	}
	tapnum = findtap(fd_ipx, local);
	if (tapnum < 0) {
		close(fd_ipx);
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot find ethertap interface: %s\n"),
			progname,
			strerror(-tapnum));
		return 1;
	}
	close(fd_ipx);
	
	fd_netlink = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_TAPBASE+tapnum);
	if (fd_netlink == -1) {
		int err = errno;
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot create NETLINK socket: %s\n"),
			progname,
			strerror(err));
		return 1;
	}
	localNET.nl_family = AF_NETLINK;
	localNET.nl_pid = serveraddr;
	localNET.nl_groups = ~0;
	status = bind(fd_netlink, (struct sockaddr*)&localNET, sizeof(localNET));
	if (status) {
		int err = errno;
		close(fd_netlink);
		close(fd_ip);
		fprintf(stderr, _("%s: Cannot bind requested address to NETLINK socket: %s\n"),
			progname,
			strerror(err));
		return 1;
	}
	arg = fcntl(fd_ip, F_GETFL);
	fcntl(fd_ip, F_SETFL, arg | O_NONBLOCK);
	arg = fcntl(fd_netlink, F_GETFL);
	fcntl(fd_netlink, F_SETFL, arg | O_NONBLOCK);
	maxfd = fd_ip;
	if (fd_netlink > maxfd)
		maxfd = fd_netlink;
	FD_ZERO(&rcvset);
	
	printf("Running...\n");
	
	while (1) {
		FD_SET(fd_ip, &rcvset);
		FD_SET(fd_netlink, &rcvset);
		status = select(maxfd+1, &rcvset, NULL, NULL, NULL);
		if (status > 0) {
			if (FD_ISSET(fd_ip, &rcvset)) {
				struct sockaddr_in remoteIP;
				socklen_t remoteIPlen = sizeof(remoteIP);
				status = recvfrom(fd_ip, buffer.ipx, sizeof(buffer.ipx), 0, (struct sockaddr*)&remoteIP, &remoteIPlen);	
				if (status > 0) {
					if (remoteIP.sin_port != htons(NOVELL_SCMD_PORT)) {
						/* invalid source port... faked? */
					} else if (remoteIP.sin_addr.s_addr != serveraddr) {
						/* invalid server... faked? */
					} else if (status < 2 + 2 + 1 + 1 + 12 + 12) {
						/* too short... */
					} else {
						int ipxLen = ntohs(*(u_int16_t*)(buffer.ipx+2));
						if (ipxLen > status) {
							/* too big... */
						} else {
							/* looks OK... */
							status += 2 + 6 + 6 + 2;
							
							buffer.unknown = 0;
							buffer.type = htons(0x8137);
							buffer.src[0] = 0x7E;
							buffer.src[1] = 0x01;
							memcpy(buffer.src+2, &remoteIP.sin_addr.s_addr, 4);
							buffer.dst[0] = 0x7E;
							buffer.dst[1] = 0x01;
							memcpy(buffer.dst+2, &local, 4);
							{
								struct sockaddr_nl remoteNETLINK;
								int e;
								
								remoteNETLINK.nl_family = AF_NETLINK;
								remoteNETLINK.nl_pid = 0;
								remoteNETLINK.nl_groups = 0;
								e = sendto(fd_netlink, &buffer, status, 0, (struct sockaddr*)&remoteNETLINK, sizeof(remoteNETLINK));
								if (e < 0)
									sendto(fd_netlink, &buffer, status, 0, (struct sockaddr*)&remoteNETLINK, sizeof(remoteNETLINK));
							}
						}
					}
				}
			}
			if (FD_ISSET(fd_netlink, &rcvset)) {
				struct sockaddr_in remoteNETLINK;
				socklen_t remoteNETLINKlen = sizeof(remoteNETLINK);
				status = recvfrom(fd_netlink, &buffer, sizeof(buffer), 0, (struct sockaddr*)&remoteNETLINK, &remoteNETLINKlen);	
				if (status > 0) {
					if (status < 2 + 6 + 6 + 2 + 2 + 2 + 1 + 1 + 12 + 12) {
						/* too short... faked? */
					} else if (buffer.type != htons(0x8137)) {
						/* invalid frame */
					} else {
						int ipxLen = ntohs(*(u_int16_t*)(buffer.ipx+2));
						status -= 2 + 6 + 6 + 2;
						if (ipxLen > status) {
							/* too big... */
						} else {
							/* looks OK... */
							{
								struct sockaddr_in remoteIP;
								int e;
								
								remoteIP.sin_family = AF_INET;
								remoteIP.sin_addr.s_addr = serveraddr;
								remoteIP.sin_port = htons(NOVELL_SCMD_PORT);
								e = sendto(fd_ip, buffer.ipx, status, 0, (struct sockaddr*)&remoteIP, sizeof(remoteIP));
								if (e < 0)
									sendto(fd_ip, buffer.ipx, status, 0, (struct sockaddr*)&remoteIP, sizeof(remoteIP));
							}
						}
					}
				}
			}
		}
	}
	close(fd_netlink);
	close(fd_ip);
	return 0;
}

