view ipv6.c @ 1693:829716214dbc draft dhcpcd-5.6.0

Improve IPv6 RA support by allowing dhcpcd to manage the address and routes instead of the kernel. dhcpcd will only do this if RA is disabled in the kernel or dhcpcd has been instructed to do this via dhcpcd.conf(5) ipv6ra_own and ipv6ra_own_default directives. Send and process IPv6 Neighbor Solicitions and Adverts to prove router reachability. If a router cannot be reached in this way then it is expired. When debugging, all ND messages are displayed which will create a lot of log spam. To ease packaging, ./configure now accepts LDFLAGS and --enable-static.
author Roy Marples <roy@marples.name>
date Thu, 05 Jul 2012 16:37:41 +0000
parents
children 2f2700ce3dba
line wrap: on
line source

/* 
 * dhcpcd - DHCP client daemon
 * Copyright (c) 2006-2012 Roy Marples <roy@marples.name>
 * 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.
 *
 * 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 <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>

#include <errno.h>
#include <ifaddrs.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "common.h"
#include "configure.h"
#include "dhcpcd.h"
#include "ipv6.h"
#include "ipv6rs.h"

/* Hackery at it's finest. */
#ifndef s6_addr32
#  define s6_addr32 __u6_addr.__u6_addr32
#endif

int socket_afnet6;
static struct rt6head *routes;

#ifdef DEBUG_MEMORY
static void
ipv6_cleanup()
{

	free(routes);
}
#endif

int
ipv6_open(void)
{
	socket_afnet6 = socket(AF_INET6, SOCK_DGRAM, 0);
	if (socket_afnet6 == -1)
		return -1;
	set_cloexec(socket_afnet6);
	routes = xmalloc(sizeof(*routes));
	TAILQ_INIT(routes);
#ifdef DEBUG_MEMORY
	atexit(ipv6_cleanup);
#endif
	return socket_afnet6;
}

struct in6_addr *
ipv6_linklocal(const char *ifname)
{
	struct ifaddrs *ifaddrs, *ifa;
	struct sockaddr_in6 *sa6;
	struct in6_addr *in6;

	if (getifaddrs(&ifaddrs) == -1)
		return NULL;

	for (ifa = ifaddrs; ifa; ifa = ifa->ifa_next) {
		if (ifa->ifa_addr == NULL ||
		    ifa->ifa_addr->sa_family != AF_INET6)
			continue;
		if (strcmp(ifa->ifa_name, ifname))
			continue;
		sa6 = (struct sockaddr_in6 *)(void *)ifa->ifa_addr;
		if (IN6_IS_ADDR_LINKLOCAL(&sa6->sin6_addr))
			break;
	}

	if (ifa) {
		in6 = xmalloc(sizeof(*in6));
		memcpy(in6, &sa6->sin6_addr, sizeof(*in6));
	} else
		in6 = NULL;

	freeifaddrs(ifaddrs);
	return in6;
}

int
ipv6_makeaddr(struct in6_addr *addr, const char *ifname,
    const struct in6_addr *prefix, int prefix_len)
{
	struct in6_addr *lla;

	if (prefix_len > 64) {
		errno = EINVAL;
		return -1;
	}

	lla = ipv6_linklocal(ifname);
	if (lla == NULL) {
		errno = ENOENT;
		return -1;
	}

	memcpy(addr, prefix, sizeof(*prefix));
	addr->s6_addr32[2] = lla->s6_addr32[2];
	addr->s6_addr32[3] = lla->s6_addr32[3];
	free(lla);
	return 0;
}

int
ipv6_mask(struct in6_addr *mask, int len)
{
	static const unsigned char masks[NBBY] =
	    { 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff };
	int bytes, bits, i;

	if (len < 0 || len > 128) {
		errno = EINVAL;
		return -1;
	}

	memset(mask, 0, sizeof(*mask));
	bytes = len / NBBY;
	bits = len % NBBY;
	for (i = 0; i < bytes; i++)
		mask->s6_addr[i] = 0xff;
	if (bits)
		mask->s6_addr[bytes] = masks[bits - 1]; 
	return 0;
}

int
ipv6_prefixlen(const struct in6_addr *mask)
{
	int x = 0, y;
	const unsigned char *lim, *p;

	lim = (const unsigned char *)mask + sizeof(*mask);
	for (p = (const unsigned char *)mask; p < lim; x++, p++) {
		if (*p != 0xff)
			break;
	}
	y = 0;
	if (p < lim) {
		for (y = 0; y < NBBY; y++) {
			if ((*p & (0x80 >> y)) == 0)
				break;
		}
	}

	/*
	 * when the limit pointer is given, do a stricter check on the
	 * remaining bits.
	 */
	if (p < lim) {
		if (y != 0 && (*p & (0x00ff >> y)) != 0)
			return -1;
		for (p = p + 1; p < lim; p++)
			if (*p != 0)
				return -1;
	}

	return x * NBBY + y;
}

static struct rt6 *
find_route6(struct rt6head *rts, const struct rt6 *r)
{
	struct rt6 *rt;

	TAILQ_FOREACH(rt, rts, next) {
		if (IN6_ARE_ADDR_EQUAL(&rt->dest, &r->dest) &&
#if HAVE_ROUTE_METRIC
		    rt->iface->metric == r->iface->metric &&
#endif
		    IN6_ARE_ADDR_EQUAL(&rt->net, &r->net))
			return rt;
	}
	return NULL;
}

static void
desc_route(const char *cmd, const struct rt6 *rt)
{
	char destbuf[INET6_ADDRSTRLEN];
	char gatebuf[INET6_ADDRSTRLEN];
	const char *ifname = rt->iface->name, *dest, *gate;

	dest = inet_ntop(AF_INET6, &rt->dest.s6_addr,
	    destbuf, INET6_ADDRSTRLEN);
	gate = inet_ntop(AF_INET6, &rt->gate.s6_addr,
	    gatebuf, INET6_ADDRSTRLEN);
	if (IN6_ARE_ADDR_EQUAL(&rt->gate, &in6addr_any))
		syslog(LOG_DEBUG, "%s: %s route to %s/%d", ifname, cmd,
		    dest, ipv6_prefixlen(&rt->net));
	else if (IN6_ARE_ADDR_EQUAL(&rt->dest, &in6addr_any) &&
	    IN6_ARE_ADDR_EQUAL(&rt->net, &in6addr_any))
		syslog(LOG_DEBUG, "%s: %s default route via %s", ifname, cmd,
		    gate);
	else
		syslog(LOG_DEBUG, "%s: %s route to %s/%d via %s", ifname, cmd,
		    dest, ipv6_prefixlen(&rt->net), gate);
}

static int
n_route(struct rt6 *rt)
{

	/* Don't set default routes if not asked to */
	if (IN6_IS_ADDR_UNSPECIFIED(&rt->dest) &&
	    IN6_IS_ADDR_UNSPECIFIED(&rt->net) &&
	    !(rt->iface->state->options->options & DHCPCD_GATEWAY))
		return -1;

	/* Delete the route first as it could exist prior to dhcpcd running
	 * and we need to ensure it leaves via our preffered interface */
	del_route6(rt);
	desc_route("adding", rt);
	if (!add_route6(rt))
		return 0;

	syslog(LOG_ERR, "%s: add_route: %m", rt->iface->name);
	return -1;
}

static int
c_route(struct rt6 *ort, struct rt6 *nrt)
{

	/* Don't set default routes if not asked to */
	if (IN6_IS_ADDR_UNSPECIFIED(&nrt->dest) &&
	    IN6_IS_ADDR_UNSPECIFIED(&nrt->net) &&
	    !(nrt->iface->state->options->options & DHCPCD_GATEWAY))
		return -1;

	desc_route("changing", nrt);
	/* We delete and add the route so that we can change metric.
	 * This also has the nice side effect of flushing ARP entries so
	 * we don't have to do that manually. */
	del_route6(ort);
	if (!add_route6(nrt))
		return 0;
	syslog(LOG_ERR, "%s: add_route: %m", nrt->iface->name);
	return -1;
}

static int
d_route(struct rt6 *rt)
{
	int retval;

	desc_route("deleting", rt);
	retval = del_route6(rt);
	if (retval != 0 && errno != ENOENT && errno != ESRCH)
		syslog(LOG_ERR,"%s: del_route: %m", rt->iface->name);
	return retval;
}

static struct rt6 *
make_route(struct ra *rap)
{
	struct rt6 *r;

	r = xzalloc(sizeof(*r));
	r->ra = rap;
	r->iface = rap->iface;
	r->metric = rap->iface->metric;
	r->mtu = rap->mtu;
	return r;
}

static struct rt6 *
make_prefix(struct ra *rap, struct ipv6_addr *addr)
{
	struct rt6 *r;

	if (addr == NULL || addr->prefix_len > 128)
		return NULL;

	r = make_route(rap);
	r->dest = addr->prefix;
	ipv6_mask(&r->net, addr->prefix_len);
	r->gate = in6addr_any;
	return r;
}

static struct rt6 *
make_router(struct ra *rap)
{
	struct rt6 *r;

	r = make_route(rap);
	r->dest = in6addr_any;
	r->net = in6addr_any;
	r->gate = rap->from;
	return r;
}

int
ipv6_remove_subnet(struct ra *rap, struct ipv6_addr *addr)
{
	struct rt6 *rt;
	int r;

	/* We need to delete the subnet route to have our metric or
	 * prefer the interface. */
	r = 0;
	rt = make_prefix(rap, addr);
	if (rt) {
		rt->iface = rap->iface;
#ifdef __linux__
		rt->metric = 256;
#else
		rt->metric = 0;
#endif
		if (!find_route6(routes, rt))
			r = del_route6(rt);
		free(rt);
	}
	return r;
}

#define RT_IS_DEFAULT(rtp) \
	(IN6_ARE_ADDR_EQUAL(&((rtp)->dest), &in6addr_any) &&		      \
	    IN6_ARE_ADDR_EQUAL(&((rtp)->net), &in6addr_any))

void
ipv6_build_routes(void)
{
	struct rt6head dnr, *nrs;
	struct rt6 *rt, *rtn, *or;
	struct ra *rap, *ran;
	struct ipv6_addr *addr;
	int have_default;

	if (!(options & (DHCPCD_IPV6RA_OWN | DHCPCD_IPV6RA_OWN_DEFAULT)))
		return;

	TAILQ_INIT(&dnr);
	TAILQ_FOREACH(rap, &ipv6_routers, next) {
		if (rap->expired)
			continue;
		if (options & DHCPCD_IPV6RA_OWN)
			TAILQ_FOREACH(addr, &rap->addrs, next) {
				rt = make_prefix(rap, addr);
				if (rt)
					TAILQ_INSERT_TAIL(&dnr, rt, next);
			}
		rt = make_router(rap);
		if (rt)
			TAILQ_INSERT_TAIL(&dnr, rt, next);
	}

	nrs = xmalloc(sizeof(*nrs));
	TAILQ_INIT(nrs);
	have_default = 0;
	TAILQ_FOREACH_SAFE(rt, &dnr, next, rtn) {
		/* Is this route already in our table? */
		if (find_route6(nrs, rt) != NULL)
			continue;
		//rt->src.s_addr = ifp->addr.s_addr;
		/* Do we already manage it? */
		if ((or = find_route6(routes, rt))) {
			if (or->iface != rt->iface ||
		//	    or->src.s_addr != ifp->addr.s_addr ||
			    !IN6_ARE_ADDR_EQUAL(&rt->gate, &or->gate) ||
			    rt->metric != or->metric)
			{
				if (c_route(or, rt) != 0)
					continue;
			}
			TAILQ_REMOVE(routes, or, next);
			free(or);
		} else {
			if (n_route(rt) != 0)
				continue;
		}
		if (RT_IS_DEFAULT(rt))
			have_default = 1;
		TAILQ_REMOVE(&dnr, rt, next);
		TAILQ_INSERT_TAIL(nrs, rt, next);
	}

	/* Free any routes we failed to add/change */
	while ((rt = TAILQ_FIRST(&dnr))) {
		TAILQ_REMOVE(&dnr, rt, next);
		free(rt);
	}

	/* Remove old routes we used to manage
	 * If we own the default route, but not RA management itself
	 * then we need to preserve the last best default route we had */
	TAILQ_FOREACH_SAFE(rt, routes, next, rtn) {
		if (find_route6(nrs, rt) == NULL) {
			if (!have_default &&
			    (options & DHCPCD_IPV6RA_OWN_DEFAULT) &&
			    !(options & DHCPCD_IPV6RA_OWN) &&
			    RT_IS_DEFAULT(rt))
				have_default = 1;
				/* no need to add it back to our routing table
				 * as we delete an exiting route when we add
				 * a new one */
			else
				d_route(rt);
		}
		free(rt);
	}
	free(routes);
	routes = nrs;

	/* Now drop expired routers */
	TAILQ_FOREACH_SAFE(rap, &ipv6_routers, next, ran) {
		if (rap->expired)
			ipv6rs_drop_ra(rap);
	}
}