/* $USAGI: ni_ifaddrs.c,v 1.8 2007-10-11 06:25:21 yoshfuji Exp $ */
/*
 * Copyright (C) 2002 USAGI/WIDE Project.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/* reformatted by indent -kr -i8 -l 1000 */
/* USAGI: ifaddrs.c,v 1.18 2002/03/06 01:50:46 yoshfuji Exp */

/**************************************************************************
 * ifaddrs.c
 * Copyright (C)2000 Hideaki YOSHIFUJI, All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <string.h>
#include <time.h>
#include <malloc.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>	/* the L2 protocols */
#include <sys/uio.h>
#include <net/if.h>
#include <net/if_arp.h>
#include "ni_ifaddrs.h"
#include <netinet/in.h>

/* ====================================================================== */
struct nlmsg_list {
	struct nlmsg_list *nlm_next;
	struct nlmsghdr *nlh;
	int size;
	time_t seq;
};

#ifndef IFA_LOCAL
struct rtmaddr_ifamap {
	void *address;
	void *local;
	void *broadcast;
	int address_len;
	int local_len;
	int broadcast_len;
};
#endif

/* ====================================================================== */
static int nl_sendreq(int sd, int request, int flags, uint32_t *seq)
{
	char reqbuf[NLMSG_ALIGN(sizeof(struct nlmsghdr)) + NLMSG_ALIGN(sizeof(struct rtgenmsg))];
	struct sockaddr_nl nladdr;
	struct nlmsghdr *req_hdr;
	struct rtgenmsg *req_msg;
	time_t t = time(NULL);

	if (seq)
		*seq = t;
	memset(&reqbuf, 0, sizeof(reqbuf));
	req_hdr = (struct nlmsghdr *) reqbuf;
	req_msg = (struct rtgenmsg *) NLMSG_DATA(req_hdr);
	req_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*req_msg));
	req_hdr->nlmsg_type = request;
	req_hdr->nlmsg_flags = flags | NLM_F_REQUEST;
	req_hdr->nlmsg_pid = 0;
	req_hdr->nlmsg_seq = t;
	req_msg->rtgen_family = AF_UNSPEC;
	memset(&nladdr, 0, sizeof(nladdr));
	nladdr.nl_family = AF_NETLINK;
	return (sendto(sd, (void *) req_hdr, req_hdr->nlmsg_len, 0, (struct sockaddr *) &nladdr, sizeof(nladdr)));
}

static int nl_recvmsg(int sd, void *buf, size_t buflen, int *flags)
{
	struct msghdr msg;
	struct iovec iov = { buf, buflen };
	struct sockaddr_nl nladdr;
	int read_len;

	for (;;) {
		msg.msg_name = (void *) &nladdr;
		msg.msg_namelen = sizeof(nladdr);
		msg.msg_iov = &iov;
		msg.msg_iovlen = 1;
		msg.msg_control = NULL;
		msg.msg_controllen = 0;
		msg.msg_flags = 0;
		read_len = recvmsg(sd, &msg, 0);
		if ((read_len < 0 && errno == EINTR)
		    || (msg.msg_flags & MSG_TRUNC))
			continue;
		if (flags)
			*flags = msg.msg_flags;
		break;
	}
	return read_len;
}

static int nl_getmsg(int sd, uint32_t seq, struct nlmsghdr **nlhp, int *done)
{
	struct nlmsghdr *nh;
	size_t bufsize = 65536, lastbufsize = 0;
	void *buff = NULL;
	int result = 0;
	size_t read_size;
	int msg_flags;
	pid_t pid = getpid();
	for (;;) {
		void *newbuff = realloc(buff, bufsize);
		if (newbuff == NULL || bufsize < lastbufsize) {
			free(newbuff);
			result = -1;
			break;
		}
		buff = newbuff;
		result = nl_recvmsg(sd, buff, bufsize, &msg_flags);
		read_size = abs(result);
		if (result < 0 || (msg_flags & MSG_TRUNC)) {
			lastbufsize = bufsize;
			bufsize *= 2;
			continue;
		}
		if (read_size == 0)
			break;
		for (nh = (struct nlmsghdr *) buff; NLMSG_OK(nh, read_size); nh = (struct nlmsghdr *) NLMSG_NEXT(nh, read_size)) {
			if ((pid_t) nh->nlmsg_pid != pid || nh->nlmsg_seq != seq)
				continue;
			if (nh->nlmsg_type == NLMSG_DONE) {
				(*done)++;
				break;	/* ok */
			}
			if (nh->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *nlerr = (struct nlmsgerr *) NLMSG_DATA(nh);
				result = -1;
				if (nh->nlmsg_len < NLMSG_LENGTH(sizeof(struct nlmsgerr)))
					errno = EIO;
				else
					errno = -nlerr->error;
				break;
			}
		}
		break;
	}
	if (result < 0)
		if (buff) {
			int saved_errno = errno;
			free(buff);
			buff = NULL;
			errno = saved_errno;
		}
	*nlhp = (struct nlmsghdr *) buff;
	return result;
}

static int nl_getlist(int sd, uint32_t seq, int request, struct nlmsg_list **nlm_list, struct nlmsg_list **nlm_end)
{
	struct nlmsghdr *nlh = NULL;
	int status;
	int done = 0;

	status = nl_sendreq(sd, request, NLM_F_ROOT | NLM_F_MATCH, &seq);
	if (status < 0)
		return status;
	if (seq == 0)
		seq = (int) time(NULL);
	while (!done) {
		status = nl_getmsg(sd, seq, &nlh, &done);
		if (status < 0)
			return status;
		if (nlh) {
			struct nlmsg_list *nlm_next = (struct nlmsg_list *) malloc(sizeof(struct nlmsg_list));
			if (nlm_next == NULL) {
				int saved_errno = errno;
				free(nlh);
				errno = saved_errno;
				status = -1;
			} else {
				nlm_next->nlm_next = NULL;
				nlm_next->nlh = (struct nlmsghdr *) nlh;
				nlm_next->size = status;
				nlm_next->seq = seq;
				if (*nlm_list == NULL) {
					*nlm_list = nlm_next;
					*nlm_end = nlm_next;
				} else {
					(*nlm_end)->nlm_next = nlm_next;
					*nlm_end = nlm_next;
				}
			}
		}
	}
	return status >= 0 ? (int) seq : status;
}

/* ---------------------------------------------------------------------- */
static void free_nlmsglist(struct nlmsg_list *nlm0)
{
	struct nlmsg_list *nlm, *nlm_next;
	int saved_errno;
	if (!nlm0)
		return;
	saved_errno = errno;
	nlm = nlm0;
	while(nlm) {
		if(nlm->nlh)
			free(nlm->nlh);
		nlm_next = nlm->nlm_next;
		free(nlm);
		nlm = nlm_next;
	}
	errno = saved_errno;
}

static void free_data(void *data)
{
	int saved_errno = errno;
	if (data != NULL)
		free(data);
	errno = saved_errno;
}

/* ---------------------------------------------------------------------- */
static void nl_close(int sd)
{
	int saved_errno = errno;
	if (sd >= 0)
		close(sd);
	errno = saved_errno;
}

/* ---------------------------------------------------------------------- */
static int nl_open(void)
{
	struct sockaddr_nl nladdr;
	int sd;

	sd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
	if (sd < 0)
		return -1;
	memset(&nladdr, 0, sizeof(nladdr));
	nladdr.nl_family = AF_NETLINK;
	if (bind(sd, (struct sockaddr *) &nladdr, sizeof(nladdr)) < 0) {
		nl_close(sd);
		return -1;
	}
	return sd;
}

/* ====================================================================== */
int ni_ifaddrs(struct ni_ifaddrs **ifap, sa_family_t family)
{
	int sd;
	struct nlmsg_list *nlmsg_list, *nlmsg_end, *nlm;
	/* - - - - - - - - - - - - - - - */
	int icnt;
	size_t dlen, xlen;
	uint32_t max_ifindex = 0;

	pid_t pid = getpid();
	int seq = 0;
	int build;		/* 0 or 1 */

/* ---------------------------------- */
	/* initialize */
	icnt = dlen = xlen = 0;
	nlmsg_list = nlmsg_end = NULL;

	if (ifap)
		*ifap = NULL;

/* ---------------------------------- */
	/* open socket and bind */
	sd = nl_open();
	if (sd < 0)
		return -1;

/* ---------------------------------- */
	/* gather info */
	if ((seq = nl_getlist(sd, seq + 1, RTM_GETADDR, &nlmsg_list, &nlmsg_end)) < 0) {
		free_nlmsglist(nlmsg_list);
		nl_close(sd);
		return -1;
	}

/* ---------------------------------- */
	/* Estimate size of result buffer and fill it */
	for (build = 0; build <= 1; build++) {
		struct ni_ifaddrs *ifl = NULL, *ifa = NULL;
		struct nlmsghdr *nlh, *nlh0;
		uint8_t *data = NULL, *xdata = NULL;
#ifndef IFA_LOCAL
		struct rtmaddr_ifamap ifamap;
#endif

		if (build) {
			data = calloc(1, NLMSG_ALIGN(sizeof(struct ni_ifaddrs) * icnt)
				      + dlen + xlen);
			ifa = (struct ni_ifaddrs *) data;
			if (ifap != NULL)
				*ifap = ifa;
			else {
				free_data(data);
				break;
			}
			if (data == NULL) {
				free_data(data);
				break;
			}
			ifl = NULL;
			data += NLMSG_ALIGN(sizeof(struct ni_ifaddrs)) * icnt;
			xdata = data + dlen;
		}

		for (nlm = nlmsg_list; nlm; nlm = nlm->nlm_next) {
			size_t nlmlen = nlm->size;
			if (!(nlh0 = nlm->nlh))
				continue;
			for (nlh = nlh0; NLMSG_OK(nlh, nlmlen); nlh = NLMSG_NEXT(nlh, nlmlen)) {
				struct ifaddrmsg *ifam = NULL;
				struct rtattr *rta;

				size_t nlm_struct_size = 0;
				sa_family_t nlm_family = 0;
				uint32_t nlm_index = 0;
				unsigned int nlm_flags;
				size_t rtasize;

#ifndef IFA_LOCAL
				memset(&ifamap, 0, sizeof(ifamap));
#endif

				/* check if the message is what we want */
				if ((pid_t) nlh->nlmsg_pid != pid || nlh->nlmsg_seq != nlm->seq)
					continue;
				if (nlh->nlmsg_type == NLMSG_DONE) {
					break;	/* ok */
				}
				switch (nlh->nlmsg_type) {
				case RTM_NEWADDR:
					ifam = (struct ifaddrmsg *) NLMSG_DATA(nlh);
					nlm_struct_size = sizeof(*ifam);
					nlm_family = ifam->ifa_family;
					nlm_index = ifam->ifa_index;
					nlm_flags = ifam->ifa_flags;
					if (family && nlm_family != family)
						continue;
					if (build) {
						ifa->ifa_ifindex = nlm_index;
						ifa->ifa_flags = nlm_flags;
					}
					break;
				default:
					continue;
				}

				if (!build) {
					if (max_ifindex < nlm_index)
						max_ifindex = nlm_index;
				} else {
					if (ifl != NULL)
						ifl->ifa_next = ifa;
				}

				rtasize = NLMSG_PAYLOAD(nlh, nlmlen) - NLMSG_ALIGN(nlm_struct_size);
				for (rta = (struct rtattr *) (((char *) NLMSG_DATA(nlh)) + 
									NLMSG_ALIGN(nlm_struct_size)); 
				     RTA_OK(rta, (unsigned short)rtasize);
				     rta = RTA_NEXT(rta, rtasize)) {
					void *rtadata = RTA_DATA(rta);
					size_t rtapayload = RTA_PAYLOAD(rta);

					switch (nlh->nlmsg_type) {
					case RTM_NEWADDR:
						if (nlm_family == AF_PACKET)
							break;
						switch (rta->rta_type) {
#ifndef IFA_LOCAL
						case IFA_ADDRESS:
							ifamap.address = rtadata;
							ifamap.address_len = rtapayload;
							break;
						case IFA_LOCAL:
							ifamap.local = rtadata;
							ifamap.local_len = rtapayload;
							break;
						case IFA_BROADCAST:
							ifamap.broadcast = rtadata;
							ifamap.broadcast_len = rtapayload;
							break;
						case IFA_LABEL:
						case IFA_UNSPEC:
							break;
#else
						case IFA_LOCAL:
							if (!build)
								dlen += NLMSG_ALIGN(rtapayload);
							else {
								memcpy(data, rtadata, rtapayload);
								ifa->ifa_addr = data;
								data += NLMSG_ALIGN(rtapayload);
							}
							break;
#endif
						case IFA_CACHEINFO:
							if (!build)
								xlen += NLMSG_ALIGN(rtapayload);
							else {
								memcpy(xdata, rtadata, rtapayload);
								ifa->ifa_cacheinfo =
									(struct ifa_cacheinfo *) xdata;
								xdata += NLMSG_ALIGN(rtapayload);
							}
							break;
						}
					}
				}
#ifndef IFA_LOCAL
				if (nlh->nlmsg_type == RTM_NEWADDR && nlm_family != AF_PACKET) {
					if (!ifamap.local) {
						ifamap.local = ifamap.address;
						ifamap.local_len = ifamap.address_len;
					}
					if (!ifamap.address) {
						ifamap.address = ifamap.local;
						ifamap.address_len = ifamap.local_len;
					}
					if (ifamap.address_len != ifamap.local_len || 
					    (ifamap.address != NULL && 
					     memcmp(ifamap.address, ifamap.local, ifamap.address_len))) {
						/* p2p; address is peer and local is ours */
						ifamap.broadcast = ifamap.address;
						ifamap.broadcast_len = ifamap.address_len;
						ifamap.address = ifamap.local;
						ifamap.address_len = ifamap.local_len;
					}
					if (ifamap.address) {
						if (!build)
							dlen += NLMSG_ALIGN(ifamap.address_len);
						else {
							ifa->ifa_addr = (struct sockaddr *) data;
							memcpy(ifa->ifa_addr, ifamap.address, ifamap.address_len);
							data += NLMSG_ALIGN(ifamap.address_len);
						}
					}
				}
#endif
				if (!build) {
					icnt++;
				} else {
					ifl = ifa++;
				}
			}
		}
		if (!build) {
			if (icnt == 0 && (dlen + xlen == 0)) {
				if (ifap != NULL)
					*ifap = NULL;
				break;	/* cannot found any addresses */
			}
		}
	}

/* ---------------------------------- */
	/* Finalize */
	free_nlmsglist(nlmsg_list);
	nl_close(sd);
	return 0;
}

/* ---------------------------------------------------------------------- */
void ni_freeifaddrs(struct ni_ifaddrs *ifa)
{
	free(ifa);
}

