/*
 * rarpd.c	RARP daemon.
 *
 *		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.
 *
 * Authors:	Alexey Kuznetsov, <kuznet@ms2.inr.ac.ru>
 */

#include <stdio.h>
#include <syslog.h>
#include <dirent.h>
#include <malloc.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <sys/signal.h>
#include <linux/if.h>
#include <linux/if_arp.h>
#include <netinet/in.h>
#include <linux/if_packet.h>
#include <linux/filter.h>

int do_reload = 1;

int debug;
int verbose;
int ifidx;
int allow_offlink;
int only_ethers;
int all_ifaces;
int listen_arp;
char *ifname;
char *tftp_dir = "/etc/tftpboot";

extern int ether_ntohost(char *name, unsigned char *ea);
void usage(void) __attribute__((noreturn));

struct iflink
{
	struct iflink	*next;
	int	       	index;
	int		hatype;
	unsigned char	lladdr[16];
	char		name[IFNAMSIZ];
	struct ifaddr 	*ifa_list;
} *ifl_list;

struct ifaddr
{
	struct ifaddr 	*next;
	__u32		prefix;
	__u32		mask;
	__u32		local;
};

struct rarp_map
{
	struct rarp_map *next;

	int		ifindex;
	int		arp_type;
	int		lladdr_len;
	unsigned char	lladdr[16];
	__u32		ipaddr;
} *rarp_db;

void usage()
{
	fprintf(stderr, "Usage: rarpd [ -dveaA ] [ -b tftpdir ] [ interface]\n");
	exit(1);
}

void load_db(void)
{
}

void load_if(void)
{
	int fd;
	struct ifreq *ifrp, *ifend;
	struct iflink *ifl;
	struct ifaddr *ifa;
	struct ifconf ifc;
	struct ifreq ibuf[256];

	if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		syslog(LOG_ERR, "socket: %m");
		return;
	}

	ifc.ifc_len = sizeof ibuf;
	ifc.ifc_buf = (caddr_t)ibuf;
	if (ioctl(fd, SIOCGIFCONF, (char *)&ifc) < 0 ||
	    ifc.ifc_len < (int)sizeof(struct ifreq)) {
		syslog(LOG_ERR, "SIOCGIFCONF: %m");
		close(fd);
		return;
	}

	while ((ifl = ifl_list) != NULL) {
		while ((ifa = ifl->ifa_list) != NULL) {
			ifl->ifa_list = ifa->next;
			free(ifa);
		}
		ifl_list = ifl->next;
		free(ifl);
	}

	ifend = (struct ifreq *)((char *)ibuf + ifc.ifc_len);
	for (ifrp = ibuf; ifrp < ifend; ifrp++) {
		__u32 addr;
		__u32 mask;
		__u32 prefix;

		if (ifrp->ifr_addr.sa_family != AF_INET)
			continue;
		addr = ((struct sockaddr_in*)&ifrp->ifr_addr)->sin_addr.s_addr;
		if (addr == 0)
			continue;
		if (ioctl(fd, SIOCGIFINDEX, ifrp)) {
			syslog(LOG_ERR, "ioctl(SIOCGIFNAME): %m");
			continue;
		}
		if (ifidx && ifrp->ifr_ifindex != ifidx)
			continue;
		for (ifl = ifl_list; ifl; ifl = ifl->next)
			if (ifl->index == ifrp->ifr_ifindex)
				break;
		if (ifl == NULL) {
			char *p;
			int index = ifrp->ifr_ifindex;

			if (ioctl(fd, SIOCGIFHWADDR, ifrp)) {
				syslog(LOG_ERR, "ioctl(SIOCGIFHWADDR): %m");
				continue;
			}

			ifl = (struct iflink*)malloc(sizeof(*ifl));
			if (ifl == NULL)
				continue;
			memset(ifl, 0, sizeof(*ifl));
			ifl->next = ifl_list;
			ifl_list = ifl;
			ifl->index = index;
			ifl->hatype = ifrp->ifr_hwaddr.sa_family;
			memcpy(ifl->lladdr, ifrp->ifr_hwaddr.sa_data, 14);
			strncpy(ifl->name, ifrp->ifr_name, IFNAMSIZ);
			p = strchr(ifl->name, ':');
			if (p)
				*p = 0;
			if (verbose)
				syslog(LOG_INFO, "link %s", ifl->name);
		}
		if (ioctl(fd, SIOCGIFNETMASK, ifrp)) {
			syslog(LOG_ERR, "ioctl(SIOCGIFMASK): %m");
			continue;
		}
		mask = ((struct sockaddr_in*)&ifrp->ifr_netmask)->sin_addr.s_addr;
		if (ioctl(fd, SIOCGIFDSTADDR, ifrp)) {
			syslog(LOG_ERR, "ioctl(SIOCGIFDSTADDR): %m");
			continue;
		}
		prefix = ((struct sockaddr_in*)&ifrp->ifr_dstaddr)->sin_addr.s_addr;
		for (ifa = ifl->ifa_list; ifa; ifa = ifa->next) {
			if (ifa->local == addr &&
			    ifa->prefix == prefix &&
			    ifa->mask == mask)
				break;
		}
		if (ifa == NULL) {
			if (mask == 0 || prefix == 0)
				continue;
			ifa = (struct ifaddr*)malloc(sizeof(*ifa));
			memset(ifa, 0, sizeof(*ifa));
			ifa->local = addr;
			ifa->prefix = prefix;
			ifa->mask = mask;
			ifa->next = ifl->ifa_list;
			ifl->ifa_list = ifa;

			if (verbose) {
				int i;
				__u32 m = ~0U;
				for (i=32; i>=0; i--) {
					if (htonl(m) == mask)
						break;
					m <<= 1;
				}
				if (addr == prefix) {
					syslog(LOG_INFO, "  addr %s/%d on %s\n",
					       inet_ntoa(*(struct in_addr*)&addr), i, ifl->name);
				} else {
					char tmpa[64];
					sprintf(tmpa, "%s", inet_ntoa(*(struct in_addr*)&addr));
					syslog(LOG_INFO, "  addr %s %s/%d on %s\n", tmpa,
					       inet_ntoa(*(struct in_addr*)&prefix), i, ifl->name);
				}
			}
		}
	}
}

void configure(void)
{
	load_if();
	load_db();
}

int bootable(__u32 addr)
{
	struct dirent *dent;
	DIR *d;
	char name[9];

	sprintf(name, "%08X", (__u32)ntohl(addr));
	d = opendir(tftp_dir);
	if (d == NULL) {
		syslog(LOG_ERR, "opendir: %m");
		return 0;
	}
	while ((dent = readdir(d)) != NULL) {
		if (strncmp(dent->d_name, name, 8) == 0)
			break;
	}
	closedir(d);
	return dent != NULL;
}

struct ifaddr *select_ipaddr(int ifindex, __u32 *sel_addr, __u32 **alist)
{
	struct iflink *ifl;
	struct ifaddr *ifa;
	int retry = 0;
	int i;

retry:
	for (ifl=ifl_list; ifl; ifl=ifl->next)
		if (ifl->index == ifindex)
			break;
	if (ifl == NULL && !retry) {
		retry++;
		load_if();
		goto retry;
	}
	if (ifl == NULL)
		return NULL;

	for (i=0; alist[i]; i++) {
		__u32 addr = *(alist[i]);
		for (ifa=ifl->ifa_list; ifa; ifa=ifa->next) {
			if (!((ifa->prefix^addr)&ifa->mask)) {
				*sel_addr = addr;
				return ifa;
			}
		}
		if (ifa == NULL && retry==0) {
			retry++;
			load_if();
			goto retry;
		}
	}
	if (i==1 && allow_offlink) {
		*sel_addr = *(alist[0]);
		return ifl->ifa_list;
	}
	syslog(LOG_ERR, "Off-link request on %s", ifl->name);
	return NULL;
}

struct rarp_map *rarp_lookup(int ifindex, int hatype,
			     int halen, unsigned char *lladdr)
{
	struct rarp_map *r;

	for (r=rarp_db; r; r=r->next) {
		if (r->arp_type != hatype && r->arp_type != -1)
			continue;
		if (r->lladdr_len != halen)
			continue;
		if (r->ifindex != ifindex && r->ifindex != 0)
			continue;
		if (memcmp(r->lladdr, lladdr, halen) == 0)
			break;
	}

	if (r == NULL) {
		if (hatype == ARPHRD_ETHER && halen == 6) {
			struct ifaddr *ifa;
			struct hostent *hp;
			char ename[256];
			static struct rarp_map emap = {
				NULL,
				0,
				ARPHRD_ETHER,
				6,
			};

			if (ether_ntohost(ename, lladdr) != 0 ||
			    (hp = gethostbyname(ename)) == NULL) {
				if (verbose)
					syslog(LOG_INFO, "not found in /etc/ethers");
				return NULL;
			}
			if (hp->h_addrtype != AF_INET) {
				syslog(LOG_ERR, "no IP address");
				return NULL;
			}
			ifa = select_ipaddr(ifindex, &emap.ipaddr, (__u32 **)hp->h_addr_list);
			if (ifa) {
				memcpy(emap.lladdr, lladdr, 6);
				if (only_ethers || bootable(emap.ipaddr))
					return &emap;
				if (verbose)
					syslog(LOG_INFO, "not bootable");
			}
		}
	}
	return r;
}

static int load_arp_bpflet(int fd)
{
	static struct sock_filter insns[] = {
		BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 6),
		BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ARPOP_RREQUEST, 0, 1),
		BPF_STMT(BPF_RET|BPF_K, 1024),
		BPF_STMT(BPF_RET|BPF_K, 0),
	};
	static struct sock_fprog filter = {
		sizeof insns / sizeof(insns[0]),
		insns
	};

	return setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));
}

int put_mylladdr(unsigned char **ptr_p, int ifindex, int alen)
{
	struct iflink *ifl;

	for (ifl=ifl_list; ifl; ifl = ifl->next)
		if (ifl->index == ifindex)
			break;

	if (ifl==NULL)
		return -1;

	memcpy(*ptr_p, ifl->lladdr, alen);
	*ptr_p += alen;
	return 0;
}

int put_myipaddr(unsigned char **ptr_p, int ifindex, __u32 hisipaddr)
{
	__u32 laddr = 0;
	struct iflink *ifl;
	struct ifaddr *ifa;

	for (ifl=ifl_list; ifl; ifl = ifl->next)
		if (ifl->index == ifindex)
			break;

	if (ifl==NULL)
		return -1;

	for (ifa=ifl->ifa_list; ifa; ifa=ifa->next) {
		if (!((ifa->prefix^hisipaddr)&ifa->mask)) {
			laddr = ifa->local;
			break;
		}
	}
	memcpy(*ptr_p, &laddr, 4);
	*ptr_p += 4;
	return 0;
}

void arp_advise(int ifindex, unsigned char *lladdr, int lllen, __u32 ipaddr)
{
	int fd;
	struct arpreq req;
	struct sockaddr_in *sin;
	struct iflink *ifl;

	for (ifl=ifl_list; ifl; ifl = ifl->next)
		if (ifl->index == ifindex)
			break;

	if (ifl == NULL)
		return;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&req, 0, sizeof(req));
	req.arp_flags = ATF_COM;
	sin = (struct sockaddr_in *)&req.arp_pa;
	sin->sin_family = AF_INET;
	sin->sin_addr.s_addr = ipaddr;
	req.arp_ha.sa_family = ifl->hatype;
	memcpy(req.arp_ha.sa_data, lladdr, lllen);
	memcpy(req.arp_dev, ifl->name, IFNAMSIZ);

	if (ioctl(fd, SIOCSARP, &req))
		syslog(LOG_ERR, "SIOCSARP: %m");
	close(fd);
}

void serve_it(int fd)
{
	unsigned char buf[1024];
	struct sockaddr_ll sll;
	socklen_t sll_len = sizeof(sll);
	struct arphdr *a = (struct arphdr*)buf;
	struct rarp_map *rmap;
	unsigned char *ptr;
	int n;

	n = recvfrom(fd, buf, sizeof(buf), MSG_DONTWAIT, (struct sockaddr*)&sll, &sll_len);
	if (n<0) {
		if (errno != EINTR && errno != EAGAIN)
			syslog(LOG_ERR, "recvfrom: %m");
		return;
	}

	/* Do not accept packets for other hosts and our own ones */
	if (sll.sll_pkttype != PACKET_BROADCAST &&
	    sll.sll_pkttype != PACKET_MULTICAST &&
	    sll.sll_pkttype != PACKET_HOST)
		return;

	if (ifidx && sll.sll_ifindex != ifidx)
		return;

	if (n<sizeof(*a)) {
		syslog(LOG_ERR, "truncated arp packet; len=%d", n);
		return;
	}

	/* Accept only RARP requests */
	if (a->ar_op != htons(ARPOP_RREQUEST))
		return;

	if (verbose) {
		int i;
		char tmpbuf[16*3];
		char *ptr = tmpbuf;
		for (i=0; i<sll.sll_halen; i++) {
			if (i) {
				sprintf(ptr, ":%02x", sll.sll_addr[i]);
				ptr++;
			} else
				sprintf(ptr, "%02x", sll.sll_addr[i]);
			ptr += 2;
		}
		syslog(LOG_INFO, "RARP request from %s on if%d", tmpbuf, sll.sll_ifindex);
	}

	/* Sanity checks */

	/* 1. IP only -> pln==4 */
	if (a->ar_pln != 4) {
		syslog(LOG_ERR, "interesting rarp_req plen=%d", a->ar_pln);
		return;
	}
	/* 2. ARP protocol must be IP */
	if (a->ar_pro != htons(ETH_P_IP)) {
		syslog(LOG_ERR, "rarp protocol is not IP %04x", ntohs(a->ar_pro));
		return;
	}
	/* 3. ARP types must match */
	if (htons(sll.sll_hatype) != a->ar_hrd) {
		switch (sll.sll_hatype) {
		case ARPHRD_FDDI:
			if (a->ar_hrd == htons(ARPHRD_ETHER) ||
			    a->ar_hrd == htons(ARPHRD_IEEE802))
				break;
		default:
			syslog(LOG_ERR, "rarp htype mismatch");
			return;
		}
	}
	/* 3. LL address lengths must be equal */
	if (a->ar_hln != sll.sll_halen) {
		syslog(LOG_ERR, "rarp hlen mismatch");
		return;
	}
	/* 4. Check packet length */
	if (sizeof(*a) + 2*4 + 2*a->ar_hln > n) {
		syslog(LOG_ERR, "truncated rarp request; len=%d", n);
		return;
	}
	/* 5. Silly check: if this guy set different source
	      addresses in MAC header and in ARP, he is insane
	 */
	if (memcmp(sll.sll_addr, a+1, sll.sll_halen)) {
		syslog(LOG_ERR, "this guy set different his lladdrs in arp and header");
		return;
	}
	/* End of sanity checks */

	/* Lookup requested target in our database */
	rmap = rarp_lookup(sll.sll_ifindex, sll.sll_hatype,
			   sll.sll_halen, (unsigned char*)(a+1) + sll.sll_halen + 4);
	if (rmap == NULL)
		return;

	/* Prepare reply. It is almost ready, we only
	   replace ARP packet type, put our lladdr and
	   IP address to source fileds,
	   and fill target IP address.
	 */
	a->ar_op = htons(ARPOP_RREPLY);
	ptr = (unsigned char*)(a+1);
	if (put_mylladdr(&ptr, sll.sll_ifindex, rmap->lladdr_len))
		return;
	if (put_myipaddr(&ptr, sll.sll_ifindex, rmap->ipaddr))
		return;
	/* It is already filled */
	ptr += rmap->lladdr_len;
	memcpy(ptr, &rmap->ipaddr, 4);
	ptr += 4;

	/* Update our ARP cache. Probably, this guy
	   will not able to make ARP (if it is broken)
	 */
	arp_advise(sll.sll_ifindex, rmap->lladdr, rmap->lladdr_len, rmap->ipaddr);

	/* Sendto is blocking, but with 5sec timeout */
	alarm(5);
	sendto(fd, buf, ptr - buf, 0, (struct sockaddr*)&sll, sizeof(sll));
	alarm(0);
}

void catch_signal(int sig, void (*handler)(int))
{
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = handler;
#ifdef SA_INTERRUPT
	sa.sa_flags = SA_INTERRUPT;
#endif
	sigaction(sig, &sa, NULL);
}

void sig_alarm(int signo)
{
}

void sig_hup(int signo)
{
	do_reload = 1;
}

int main(int argc, char **argv)
{
	struct pollfd pset[2];
	int psize;
	int opt;


	opterr = 0;
	while ((opt = getopt(argc, argv, "aAb:dvoe")) != EOF) {
		switch (opt) {
		case 'a':
			++all_ifaces;
			break;

		case 'A':
			++listen_arp;
			break;

		case 'd':
			++debug;
			break;

		case 'v':
			++verbose;
			break;

		case 'o':
			++allow_offlink;
			break;

		case 'e':
			++only_ethers;
			break;

		case 'b':
			tftp_dir = optarg;
			break;

		default:
			usage();
		}
	}
	if (argc > optind) {
		if (argc > optind+1)
			usage();
		ifname = argv[optind];
	}

	psize = 1;
	pset[0].fd = socket(PF_PACKET, SOCK_DGRAM, 0);

	if (ifname) {
		struct ifreq ifr;
		memset(&ifr, 0, sizeof(ifr));
		strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
		if (ioctl(pset[0].fd, SIOCGIFINDEX, &ifr)) {
			perror("ioctl(SIOCGIFINDEX)");
			usage();
		}
		ifidx = ifr.ifr_ifindex;
	}

	pset[1].fd = -1;
	if (listen_arp) {
		pset[1].fd = socket(PF_PACKET, SOCK_DGRAM, 0);
		if (pset[1].fd >= 0) {
			load_arp_bpflet(pset[1].fd);
			psize = 1;
		}
	}

	if (pset[1].fd >= 0) {
		struct sockaddr_ll sll;
		memset(&sll, 0, sizeof(sll));
		sll.sll_family = AF_PACKET;
		sll.sll_protocol = htons(ETH_P_ARP);
		sll.sll_ifindex = all_ifaces ? 0 : ifidx;
		if (bind(pset[1].fd, (struct sockaddr*)&sll, sizeof(sll)) < 0) {
			close(pset[1].fd);
			pset[1].fd = -1;
			psize = 1;
		}
	}
	if (pset[0].fd >= 0) {
		struct sockaddr_ll sll;
		memset(&sll, 0, sizeof(sll));
		sll.sll_family = AF_PACKET;
		sll.sll_protocol = htons(ETH_P_RARP);
		sll.sll_ifindex = all_ifaces ? 0 : ifidx;
		if (bind(pset[0].fd, (struct sockaddr*)&sll, sizeof(sll)) < 0) {
			close(pset[0].fd);
			pset[0].fd = -1;
		}
	}
	if (pset[0].fd < 0) {
		pset[0] = pset[1];
		psize--;
	}
	if (psize == 0) {
		fprintf(stderr, "failed to bind any socket. Aborting.\n");
		exit(1);
	}

	if (!debug) {
		int fd;
		pid_t pid = fork();

		if (pid > 0)
			exit(0);
		else if (pid == -1) {
			perror("rarpd: fork");
			exit(1);
		}

		if (chdir("/") < 0) {
			perror("rarpd: chdir");
			exit(1);
		}

		fd = open("/dev/null", O_RDWR);
		if (fd >= 0) {
			dup2(fd, 0);
			dup2(fd, 1);
			dup2(fd, 2);
			if (fd > 2)
				close(fd);
		}
		setsid();
	}

	openlog("rarpd", LOG_PID | LOG_CONS, LOG_DAEMON);
	catch_signal(SIGALRM, sig_alarm);
	catch_signal(SIGHUP, sig_hup);

	for (;;) {
		int i;

		if (do_reload) {
			configure();
			do_reload = 0;
		}

#define EVENTS (POLLIN|POLLPRI|POLLERR|POLLHUP)
		pset[0].events = EVENTS;
		pset[0].revents = 0;
		pset[1].events = EVENTS;
		pset[1].revents = 0;

		i = poll(pset, psize, -1);
		if (i <= 0) {
			if (errno != EINTR && i<0) {
				syslog(LOG_ERR, "poll returned some crap: %m\n");
				sleep(10);
			}
			continue;
		}
		for (i=0; i<psize; i++) {
			if (pset[i].revents&EVENTS)
				serve_it(pset[i].fd);
		}
	}
}
