/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2002 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*	Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T	*/
/*	  All Rights Reserved  	*/

/*
 * Portions of this source code were derived from Berkeley 4.3 BSD
 * under license from the Regents of the University of California.
 */

#include "defs.h"

#define	IF_SEPARATOR	':'

struct interface	*ifnet;

static int		setup_listen_sock(int ifindex);
static void		addrouteforif(struct interface *ifp);
static void		resetup_listen_sock(struct interface *, int);

/*
 * This is called at startup and after that, every CHECK_INTERVAL seconds or
 * when a SIGHUP is received.
 */
void
initifs(void)
{
	static char *buf = NULL;
	static uint_t maxbufsize = 0;
	int bufsize;
	int numifs;
	struct lifnum lifn;
	struct lifconf lifc;
	struct lifreq lifr;
	struct lifreq *lifrp;
	int n;
	struct interface ifs;
	struct interface *ifp;
	int netmaskchange = 0;
	boolean_t changes = _B_FALSE;

	lifn.lifn_family = AF_INET6;
	lifn.lifn_flags = 0;
	if (ioctl(iocsoc, SIOCGLIFNUM, (char *)&lifn) < 0) {
		syslog(LOG_ERR, "initifs: ioctl (get interface numbers): %m");
		return;
	}
	numifs = lifn.lifn_count;
	bufsize = numifs * sizeof (struct lifreq);

	if (buf == NULL || bufsize > maxbufsize) {
		if (buf != NULL)
			free(buf);
		maxbufsize = bufsize;
		buf = (char *)malloc(maxbufsize);
		if (buf == NULL) {
			syslog(LOG_ERR, "initifs: out of memory");
			return;
		}
	}

	lifc.lifc_family = AF_INET6;
	lifc.lifc_flags = 0;
	lifc.lifc_len = bufsize;
	lifc.lifc_buf = buf;
	if (ioctl(iocsoc, SIOCGLIFCONF, (char *)&lifc) < 0) {
		syslog(LOG_ERR,
		    "initifs: ioctl (get interface configuration): %m");
		return;
	}

	/*
	 * Mark all of the currently known interfaces in order to determine
	 * which of the these interfaces no longer exist.
	 */
	for (ifp = ifnet; ifp != NULL; ifp = ifp->int_next)
		ifp->int_flags |= RIP6_IFF_MARKED;
	lifrp = lifc.lifc_req;
	for (n = lifc.lifc_len / sizeof (struct lifreq); n > 0; n--, lifrp++) {
		bzero((char *)&ifs, sizeof (ifs));
		(void) strncpy(lifr.lifr_name, lifrp->lifr_name,
		    sizeof (lifr.lifr_name));
		if (ioctl(iocsoc, SIOCGLIFFLAGS, (char *)&lifr) < 0) {
			syslog(LOG_ERR,
			    "initifs: ioctl (get interface flags): %m");
			continue;
		}
		if (!(lifr.lifr_flags & IFF_IPV6) ||
		    !(lifr.lifr_flags & IFF_MULTICAST) ||
		    (lifr.lifr_flags & IFF_LOOPBACK))
			continue;

		ifp = if_ifwithname(lifr.lifr_name);
		if (ifp != NULL)
			ifp->int_flags &= ~RIP6_IFF_MARKED;
		if (lifr.lifr_flags & IFF_POINTOPOINT)
			ifs.int_flags |= RIP6_IFF_POINTOPOINT;
		if (lifr.lifr_flags & IFF_NORTEXCH)
			ifs.int_flags |= RIP6_IFF_NORTEXCH;
		if (lifr.lifr_flags & IFF_PRIVATE)
			ifs.int_flags |= RIP6_IFF_PRIVATE;
		if (lifr.lifr_flags & IFF_UP) {
			ifs.int_flags |= RIP6_IFF_UP;
		} else {
			if (ifp != NULL) {
				if (ifp->int_flags & RIP6_IFF_UP) {
					/*
					 * If there is an transition from up to
					 * down for an exisiting interface,
					 * increment the counter.
					 */
					ifp->int_transitions++;
					changes = _B_TRUE;
				}
				if_purge(ifp);
			}
			continue;
		}

		if (ifs.int_flags & RIP6_IFF_POINTOPOINT) {
			/*
			 * For point-to-point interfaces, retrieve both the
			 * local and the remote addresses.
			 */
			if (ioctl(iocsoc, SIOCGLIFADDR, (char *)&lifr) < 0) {
				syslog(LOG_ERR,
				    "initifs: ioctl (get interface address): "
				    "%m");
				continue;
			}
			ifs.int_addr =
			    ((struct sockaddr_in6 *)&lifr.lifr_addr)->sin6_addr;
			if (ioctl(iocsoc, SIOCGLIFDSTADDR, (char *)&lifr) < 0) {
				syslog(LOG_ERR,
				    "initifs: ioctl (get destination address): "
				    "%m");
				continue;
			}
			ifs.int_dstaddr = ((struct sockaddr_in6 *)
			    &lifr.lifr_dstaddr)->sin6_addr;
			ifs.int_prefix_length = IPV6_ABITS;
		} else {
			/*
			 * For other interfaces, retreieve the prefix (including
			 * the prefix length.
			 */
			if (ioctl(iocsoc, SIOCGLIFSUBNET, (char *)&lifr) < 0) {
				syslog(LOG_ERR,
				    "initifs: ioctl (get subnet prefix): %m");
				continue;
			}
			/*
			 * This should never happen but check for it in any case
			 * since the kernel stores it as an signed integer.
			 */
			if (lifr.lifr_addrlen < 0 ||
			    lifr.lifr_addrlen > IPV6_ABITS) {
				syslog(LOG_ERR,
				    "initifs: ioctl (get subnet prefix) "
				    "returned invalid prefix length of %d",
				    lifr.lifr_addrlen);
				continue;
			}
			ifs.int_prefix_length = lifr.lifr_addrlen;
			ifs.int_addr = ((struct sockaddr_in6 *)
			    &lifr.lifr_subnet)->sin6_addr;
		}

		if (ioctl(iocsoc, SIOCGLIFMETRIC, (char *)&lifr) < 0 ||
		    lifr.lifr_metric < 0)
			ifs.int_metric = 1;
		else
			ifs.int_metric = lifr.lifr_metric + 1;

		if (ioctl(iocsoc, SIOCGLIFINDEX, (char *)&lifr) < 0) {
			syslog(LOG_ERR, "initifs: ioctl (get index): %m");
			continue;
		}
		ifs.int_ifindex = lifr.lifr_index;

		if (ioctl(iocsoc, SIOCGLIFMTU, (char *)&lifr) < 0) {
			syslog(LOG_ERR, "initifs: ioctl (get mtu): %m");
			continue;
		}

		/*
		 * If the interface's recorded MTU doesn't make sense, use
		 * IPV6_MIN_MTU instead.
		 */
		if (lifr.lifr_mtu < IPV6_MIN_MTU)
			ifs.int_mtu = IPV6_MIN_MTU;
		else
			ifs.int_mtu = lifr.lifr_mtu;

		if (ifp != NULL) {
			/*
			 * RIP6_IFF_NORTEXCH flag change by itself shouldn't
			 * cause an if_purge() call, which also purges all the
			 * routes heard off this interface. So, let's suppress
			 * changes of RIP6_IFF_NORTEXCH	in the following
			 * comparisons.
			 */
			if (ifp->int_prefix_length == ifs.int_prefix_length &&
			    ((ifp->int_flags | RIP6_IFF_NORTEXCH) ==
			    (ifs.int_flags | RIP6_IFF_NORTEXCH)) &&
			    ifp->int_metric == ifs.int_metric &&
			    ifp->int_ifindex == ifs.int_ifindex) {
				/*
				 * Now let's make sure we capture the latest
				 * value of RIP6_IFF_NORTEXCH flag.
				 */
				if (ifs.int_flags & RIP6_IFF_NORTEXCH)
					ifp->int_flags |= RIP6_IFF_NORTEXCH;
				else
					ifp->int_flags &= ~RIP6_IFF_NORTEXCH;

				if (!(ifp->int_flags & RIP6_IFF_POINTOPOINT) &&
				    IN6_ARE_ADDR_EQUAL(&ifp->int_addr,
					&ifs.int_addr))
					continue;
				if ((ifp->int_flags & RIP6_IFF_POINTOPOINT) &&
				    IN6_ARE_ADDR_EQUAL(&ifp->int_dstaddr,
					&ifs.int_dstaddr))
					continue;
			}
			if_purge(ifp);
			if (ifp->int_prefix_length != ifs.int_prefix_length)
				netmaskchange = 1;
			ifp->int_addr = ifs.int_addr;
			ifp->int_dstaddr = ifs.int_dstaddr;
			ifp->int_metric = ifs.int_metric;
			/*
			 * If there is an transition from down to up for an
			 * exisiting interface, increment the counter.
			 */
			if (!(ifp->int_flags & RIP6_IFF_UP) &&
			    (ifs.int_flags & RIP6_IFF_UP))
				ifp->int_transitions++;
			ifp->int_flags |= ifs.int_flags;
			ifp->int_prefix_length = ifs.int_prefix_length;

			/*
			 * If the interface index has changed, we may need to
			 * set up the listen socket again.
			 */
			if (ifp->int_ifindex != ifs.int_ifindex) {
				if (ifp->int_sock != -1) {
					resetup_listen_sock(ifp,
					    ifs.int_ifindex);
				}
				ifp->int_ifindex = ifs.int_ifindex;
			}

			ifp->int_mtu = ifs.int_mtu;
		} else {
			char *cp;
			int log_num;

			ifp = (struct interface *)
			    malloc(sizeof (struct interface));
			if (ifp == NULL) {
				syslog(LOG_ERR, "initifs: out of memory");
				return;
			}
			*ifp = ifs;
			ifp->int_name = ifp->int_ifbase = NULL;
			ifp->int_name =
			    (char *)malloc((size_t)strlen(lifr.lifr_name) + 1);
			if (ifp->int_name == NULL) {
				free(ifp);
				syslog(LOG_ERR, "initifs: out of memory");
				return;
			}
			(void) strcpy(ifp->int_name, lifr.lifr_name);
			ifp->int_ifbase =
			    (char *)malloc((size_t)strlen(lifr.lifr_name) + 1);
			if (ifp->int_ifbase == NULL) {
				free(ifp->int_name);
				free(ifp);
				syslog(LOG_ERR, "initifs: out of memory");
				return;
			}
			(void) strcpy(ifp->int_ifbase, lifr.lifr_name);
			cp = (char *)index(ifp->int_ifbase, IF_SEPARATOR);
			if (cp != NULL) {
				/*
				 * Verify that the value following the separator
				 * is an integer greater than zero (the only
				 * possible value for a logical interface).
				 */
				log_num = atoi((char *)(cp + 1));
				if (log_num <= 0) {
					free(ifp->int_ifbase);
					free(ifp->int_name);
					free(ifp);
					syslog(LOG_ERR,
					    "initifs: interface name %s could "
					    "not be parsed", ifp->int_name);
					return;
				}
				*cp = '\0';
			} else {
				log_num = 0;
			}
			if (log_num == 0) {
				ifp->int_sock =
				    setup_listen_sock(ifp->int_ifindex);
			} else {
				ifp->int_sock = -1;
			}
			ifp->int_next = ifnet;
			ifnet = ifp;
			traceinit(ifp);
		}
		addrouteforif(ifp);
		changes = _B_TRUE;
	}

	/*
	 * Any remaining interfaces that are still marked and which were in an
	 * up state (RIP6_IFF_UP) need to removed from the routing table.
	 */
	for (ifp = ifnet; ifp != NULL; ifp = ifp->int_next) {
		if ((ifp->int_flags & (RIP6_IFF_MARKED | RIP6_IFF_UP)) ==
		    (RIP6_IFF_MARKED | RIP6_IFF_UP)) {
			if_purge(ifp);
			ifp->int_flags &= ~RIP6_IFF_MARKED;
			changes = _B_TRUE;
		}
	}
	if (netmaskchange)
		rtchangeall();
	if (supplier & changes)
		dynamic_update((struct interface *)NULL);
}

static void
addrouteforif(struct interface *ifp)
{
	struct rt_entry *rt;
	struct in6_addr *dst;

	if (ifp->int_flags & RIP6_IFF_POINTOPOINT)
		dst = &ifp->int_dstaddr;
	else
		dst = &ifp->int_addr;

	rt = rtlookup(dst, ifp->int_prefix_length);

	if (rt != NULL) {
		if (rt->rt_state & RTS_INTERFACE)
			return;
		rtdelete(rt);
	}
	rtadd(dst, &ifp->int_addr, ifp->int_prefix_length, ifp->int_metric, 0,
	    _B_TRUE, ifp);
}

static int
setup_listen_sock(int ifindex)
{
	int sock;
	struct sockaddr_in6 sin6;
	uint_t hops;
	struct ipv6_mreq allrouters_mreq;
	int on = 1;
	int off = 0;
	int recvsize;

	sock = socket(AF_INET6, SOCK_DGRAM, 0);
	if (sock == -1)
		goto sock_fail;

	if (setsockopt(sock, IPPROTO_IPV6, IPV6_BOUND_IF, (char *)&ifindex,
	    sizeof (ifindex)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: IPV6_BOUND_IF: %m");
		goto sock_fail;
	}

	hops = IPV6_MAX_HOPS;
	if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, (char *)&hops,
	    sizeof (hops)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: IPV6_UNICAST_HOPS: %m");
		goto sock_fail;
	}

	if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (char *)&hops,
	    sizeof (hops)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: IPV6_MULTICAST_HOPS: %m");
		goto sock_fail;
	}

	if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (char *)&off,
	    sizeof (off)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: IPV6_MULTICAST_LOOP: %m");
		goto sock_fail;
	}

	allrouters_mreq.ipv6mr_multiaddr = allrouters_in6;
	allrouters_mreq.ipv6mr_interface = ifindex;
	if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP,
	    (char *)&allrouters_mreq, sizeof (allrouters_mreq)) < 0) {
		if (errno != EADDRINUSE) {
			syslog(LOG_ERR,
			    "setup_listen_sock: setsockopt: "
			    "IPV6_JOIN_GROUP: %m");
			goto sock_fail;
		}
	}

	if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, (char *)&on,
	    sizeof (off)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: IPV6_RECVHOPLIMIT: %m");
		goto sock_fail;
	}

	if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&on,
	    sizeof (on)) < 0) {
		syslog(LOG_ERR,
		    "setup_listen_sock: setsockopt: SO_REUSEADDR: %m");
		goto sock_fail;
	}

	recvsize = RCVBUFSIZ;
	if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char *)&recvsize,
	    sizeof (int)) < 0) {
		syslog(LOG_ERR, "setup_listen_sock: setsockopt: SO_RCVBUF: %m");
		goto sock_fail;
	}

	bzero((char *)&sin6, sizeof (sin6));
	sin6.sin6_family = AF_INET6;
	sin6.sin6_port = rip6_port;
	if (bind(sock, (struct sockaddr *)&sin6, sizeof (sin6)) < 0) {
		syslog(LOG_ERR, "setup_listen_sock: bind: %m");
		goto sock_fail;
	}

	poll_ifs_num++;
	if (poll_ifs == NULL) {
		poll_ifs = (struct pollfd *)
		    malloc(max_poll_ifs * sizeof (struct pollfd));
	} else if (poll_ifs_num > max_poll_ifs) {
		max_poll_ifs *= 2;
		poll_ifs = (struct pollfd *)realloc((char *)poll_ifs,
		    max_poll_ifs * sizeof (struct pollfd));
	}
	if (poll_ifs == NULL) {
		syslog(LOG_ERR, "setup_listen_sock: out of memory");
		goto sock_fail;
	}

	poll_ifs[poll_ifs_num - 1].fd = sock;
	poll_ifs[poll_ifs_num - 1].events = POLLIN;
	return (sock);

sock_fail:
	if (sock > 0)
		(void) close(sock);
	return (-1);
}

/*
 * resetup_listen_sock is primarily used in the case where a tunnel was
 * plumbed, unplumbed, then plumbed again.  This would cause the binding set by
 * IPV6_BOUND_IF to be useless, and sends to the associated socket will be
 * transmitted on the wrong interface.  resetup_listen_sock
 *	closes the socket,
 *	removes the socket from poll_ifs[]
 *	plugs the hole in poll_ifs[]
 *	calls setup_listen_sock to set up the socket again
 */
void
resetup_listen_sock(struct interface *ifp, int newindex)
{
	int i;

	(void) close(ifp->int_sock);

	/* Remove socket from poll_ifs[]. */
	for (i = poll_ifs_num - 1; i >= 0; i--) {

		if (poll_ifs[i].fd == ifp->int_sock) {

			poll_ifs[i].fd = 0;
			poll_ifs[i].events = 0;

			/*
			 * Remove hole in poll_ifs.  Possibly exchange
			 * poll_ifs[i] with poll_ifs[poll_ifs_num-1].
			 */
			if (i != poll_ifs_num - 1) {
				poll_ifs[i] = poll_ifs[poll_ifs_num - 1];
				poll_ifs[poll_ifs_num - 1].fd = 0;
				poll_ifs[poll_ifs_num - 1].events = 0;
			}
			poll_ifs_num--;

			/* Now set everything up again. */
			ifp->int_sock = setup_listen_sock(newindex);
			break;
		}
	}
}
