netconfig: Create routes from Router Advertisements

If IPv6 is enabled, create the ICMP6 client and handle Router
Advertisements received by creating, updating and removing routes per
the data in the Router Advertisements.  Timeouts aren't handled yet.
diff --git a/ell/netconfig.c b/ell/netconfig.c
index 9375d13..07783f2 100644
--- a/ell/netconfig.c
+++ b/ell/netconfig.c
@@ -27,14 +27,19 @@
 #include <linux/types.h>
 #include <netinet/ip.h>
 #include <arpa/inet.h>
+#include <netinet/icmp6.h>
 
 #include "private.h"
 #include "useful.h"
 #include "log.h"
 #include "dhcp.h"
 #include "dhcp-private.h"
+#include "icmp6.h"
+#include "icmp6-private.h"
+#include "dhcp6.h"
 #include "netlink.h"
 #include "rtnl.h"
+#include "rtnl-private.h"
 #include "queue.h"
 #include "time.h"
 #include "idle.h"
@@ -64,6 +69,8 @@
 	bool v4_configured;
 	struct l_dhcp_client *dhcp_client;
 	bool v6_configured;
+	struct l_icmp6_client *icmp6_client;
+	struct l_dhcp6_client *dhcp6_client;
 
 	/* These objects, if not NULL, are owned by @addresses and @routes */
 	struct l_rtnl_address *v4_address;
@@ -128,6 +135,33 @@
 		netconfig_update_cleanup(nc);
 }
 
+static struct l_rtnl_route *netconfig_route_new(struct l_netconfig *nc,
+						uint8_t family,
+						const void *dst,
+						uint8_t prefix_len,
+						const void *gw,
+						uint8_t protocol)
+{
+	struct l_rtnl_route *rt = l_new(struct l_rtnl_route, 1);
+
+	rt->family = family;
+	rt->scope = (family == AF_INET && dst) ?
+		RT_SCOPE_LINK : RT_SCOPE_UNIVERSE;
+	rt->protocol = protocol;
+	rt->lifetime = 0xffffffff;
+	rt->priority = nc->route_priority;
+
+	if (dst) {
+		memcpy(&rt->dst, dst, family == AF_INET ? 4 : 16);
+		rt->dst_prefix_len = prefix_len;
+	}
+
+	if (gw)
+		memcpy(&rt->gw, gw, family == AF_INET ? 4 : 16);
+
+	return rt;
+}
+
 static void netconfig_add_v4_routes(struct l_netconfig *nc, const char *ip,
 					uint8_t prefix_len, const char *gateway,
 					uint8_t rtm_protocol)
@@ -141,12 +175,9 @@
 		return;
 
 	in_addr.s_addr &= htonl(0xfffffffflu << (32 - prefix_len));
-	if (L_WARN_ON(!inet_ntop(AF_INET, &in_addr, network, INET_ADDRSTRLEN)))
-		return;
-
-	nc->v4_subnet_route = l_rtnl_route_new_prefix(network, prefix_len);
-	l_rtnl_route_set_protocol(nc->v4_subnet_route, rtm_protocol);
-	l_rtnl_route_set_priority(nc->v4_subnet_route, nc->route_priority);
+	nc->v4_subnet_route = netconfig_route_new(nc, AF_INET, network,
+							prefix_len, NULL,
+							rtm_protocol);
 	l_queue_push_tail(nc->routes.current, nc->v4_subnet_route);
 	l_queue_push_tail(nc->routes.added, nc->v4_subnet_route);
 
@@ -163,6 +194,7 @@
 	nc->v4_default_route = l_rtnl_route_new_gateway(gateway);
 	l_rtnl_route_set_protocol(nc->v4_default_route, rtm_protocol);
 	L_WARN_ON(!l_rtnl_route_set_prefsrc(nc->v4_default_route, ip));
+	l_rtnl_route_set_priority(nc->v4_default_route, nc->route_priority);
 	l_queue_push_tail(nc->routes.current, nc->v4_default_route);
 	l_queue_push_tail(nc->routes.added, nc->v4_default_route);
 }
@@ -173,7 +205,6 @@
 {
 	struct in6_addr in6_addr;
 	const void *prefix;
-	char network[INET6_ADDRSTRLEN];
 	struct l_rtnl_route *v6_subnet_route;
 	struct l_rtnl_route *v6_default_route;
 
@@ -188,17 +219,13 @@
 	 */
 	prefix = net_prefix_from_ipv6(in6_addr.s6_addr, prefix_len);
 
-	if (L_WARN_ON(!inet_ntop(AF_INET6, prefix, network, INET6_ADDRSTRLEN)))
-		return;
-
 	/*
 	 * One reason we add a subnet route instead of letting the kernel
 	 * do it, by not specifying IFA_F_NOPREFIXROUTE for the address,
 	 * is that that would force a 0 metric for the route.
 	 */
-	v6_subnet_route = l_rtnl_route_new_prefix(network, prefix_len);
-	l_rtnl_route_set_protocol(v6_subnet_route, RTPROT_STATIC);
-	l_rtnl_route_set_priority(v6_subnet_route, nc->route_priority);
+	v6_subnet_route = netconfig_route_new(nc, AF_INET6, prefix, prefix_len,
+						NULL, RTPROT_STATIC);
 	l_queue_push_tail(nc->routes.current, v6_subnet_route);
 	l_queue_push_tail(nc->routes.added, v6_subnet_route);
 
@@ -214,6 +241,19 @@
 	l_queue_push_tail(nc->routes.added, v6_default_route);
 }
 
+static bool netconfig_route_exists(struct l_queue *list,
+					const struct l_rtnl_route *route)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(list); entry;
+			entry = entry->next)
+		if ((const struct l_rtnl_route *) entry->data == route)
+			return true;
+
+	return false;
+}
+
 static void netconfig_add_dhcp_address_routes(struct l_netconfig *nc)
 {
 	const struct l_dhcp_lease *lease =
@@ -360,6 +400,204 @@
 	}
 }
 
+static struct l_rtnl_route *netconfig_find_icmp6_route(
+						struct l_netconfig *nc,
+						const uint8_t *gateway,
+						const struct route_info *dst)
+{
+	const struct l_queue_entry *entry;
+
+	for (entry = l_queue_get_entries(nc->routes.current); entry;
+			entry = entry->next) {
+		struct l_rtnl_route *route = entry->data;
+		const uint8_t *route_gateway;
+		const uint8_t *route_dst;
+		uint8_t route_prefix_len = 0;
+
+		if (l_rtnl_route_get_family(route) != AF_INET6 ||
+				l_rtnl_route_get_protocol(route) != RTPROT_RA)
+			continue;
+
+		route_gateway = l_rtnl_route_get_gateway_in_addr(route);
+		if ((gateway || route_gateway) &&
+				(!gateway || !route_gateway ||
+				 memcmp(gateway, route_gateway, 16)))
+			continue;
+
+		route_dst = l_rtnl_route_get_dst_in_addr(route,
+							&route_prefix_len);
+		if ((dst || route_prefix_len) &&
+				(!dst || !route_prefix_len ||
+				 dst->prefix_len != route_prefix_len ||
+				 memcmp(dst->address, route_dst, 16)))
+			continue;
+
+		return route;
+	}
+
+	return NULL;
+}
+
+static struct l_rtnl_route *netconfig_add_icmp6_route(struct l_netconfig *nc,
+						const uint8_t *gateway,
+						const struct route_info *dst,
+						uint8_t preference)
+{
+	struct l_rtnl_route *rt;
+
+	rt = netconfig_route_new(nc, AF_INET6, dst->address, dst->prefix_len,
+					gateway, RTPROT_RA);
+	if (L_WARN_ON(!rt))
+		return NULL;
+
+	l_rtnl_route_set_preference(rt, preference);
+	l_queue_push_tail(nc->routes.current, rt);
+	l_queue_push_tail(nc->routes.added, rt);
+	return rt;
+}
+
+static void netconfig_set_icmp6_route_data(struct l_netconfig *nc,
+						struct l_rtnl_route *rt,
+						uint64_t start_time,
+						uint32_t preferred_lifetime,
+						uint32_t valid_lifetime,
+						uint32_t mtu, bool updated)
+{
+	uint64_t expiry = start_time + valid_lifetime * L_USEC_PER_SEC;
+	uint64_t old_expiry = l_rtnl_route_get_expiry(rt);
+	bool differs = false;
+
+	if (mtu != l_rtnl_route_get_mtu(rt)) {
+		l_rtnl_route_set_mtu(rt, mtu);
+		differs = true;
+	}
+
+	/*
+	 * valid_lifetime of 0 from a route_info means the route is being
+	 * removed so we wouldn't be here.  valid_lifetime of 0xffffffff
+	 * means no timeout.  Check if the lifetime is changing between
+	 * finite and infinite, or two finite values that result in expiry
+	 * time difference of more than a second -- to avoid emitting
+	 * updates for changes resulting only from the valid_lifetime one
+	 * second resolution and RA transmission jitter.  As RFC4861
+	 * Section 6.2.7 puts it: "Due to link propagation delays and
+	 * potentially poorly synchronized clocks between the routers such
+	 * comparison SHOULD allow some time skew."  The RFC talks about
+	 * routers processing one another's RAs but the same logic applies
+	 * here.
+	 */
+	if (valid_lifetime == 0xffffffff)
+		expiry = 0;
+
+	if ((expiry || old_expiry) &&
+			(!expiry || !old_expiry ||
+			 l_time_diff(expiry, old_expiry) > L_USEC_PER_SEC)) {
+		l_rtnl_route_set_lifetime(rt, valid_lifetime);
+		l_rtnl_route_set_expiry(rt, expiry);
+		differs = true;
+	}
+
+	if (updated && differs && !netconfig_route_exists(nc->routes.added, rt))
+		l_queue_push_tail(nc->routes.updated, rt);
+}
+
+static void netconfig_remove_icmp6_route(struct l_netconfig *nc,
+						struct l_rtnl_route *route)
+{
+	l_queue_remove(nc->routes.current, route);
+	l_queue_push_tail(nc->routes.removed, route);
+}
+
+static void netconfig_icmp6_event_handler(struct l_icmp6_client *client,
+						enum l_icmp6_client_event event,
+						void *event_data,
+						void *user_data)
+{
+	struct l_netconfig *nc = user_data;
+	const struct l_icmp6_router *r;
+	struct l_rtnl_route *default_route;
+	unsigned int i;
+
+	if (event != L_ICMP6_CLIENT_EVENT_ROUTER_FOUND)
+		return;
+
+	r = event_data;
+
+	/*
+	 * Note: If this is the first RA received, the l_dhcp6_client
+	 * will have received the event before us and will be acting
+	 * on it by now.
+	 */
+
+	if (nc->v6_gateway_override)
+		return;
+
+	/* Process the default gateway information */
+	default_route = netconfig_find_icmp6_route(nc, r->address, NULL);
+
+	if (!default_route && r->lifetime) {
+		default_route = netconfig_add_icmp6_route(nc, r->address, NULL,
+								r->pref);
+		if (unlikely(!default_route))
+			return;
+
+		/*
+		 * r->lifetime is 16-bit only so there's no risk it gets
+		 * confused for the special 0xffffffff value in
+		 * netconfig_set_icmp6_route_data.
+		 */
+		netconfig_set_icmp6_route_data(nc, default_route, r->start_time,
+						r->lifetime, r->lifetime,
+						r->mtu, false);
+	} else if (default_route && r->lifetime)
+		netconfig_set_icmp6_route_data(nc, default_route, r->start_time,
+						r->lifetime, r->lifetime,
+						r->mtu, true);
+	else if (default_route && !r->lifetime)
+		netconfig_remove_icmp6_route(nc, default_route);
+
+	/*
+	 * Process the onlink and offlink routes, from the Router
+	 * Advertisement's Prefix Information options and Route
+	 * Information options respectively.
+	 */
+	for (i = 0; i < r->n_routes; i++) {
+		const struct route_info *info = &r->routes[i];
+		const uint8_t *gateway = info->onlink ? NULL : r->address;
+		struct l_rtnl_route *route =
+			netconfig_find_icmp6_route(nc, gateway, info);
+
+		if (!route && info->valid_lifetime) {
+			route = netconfig_add_icmp6_route(nc, gateway, info,
+							info->preference);
+			if (unlikely(!route))
+				continue;
+
+			netconfig_set_icmp6_route_data(nc, route, r->start_time,
+						info->preferred_lifetime,
+						info->valid_lifetime,
+						gateway ? r->mtu : 0, false);
+		} else if (route && info->valid_lifetime)
+			netconfig_set_icmp6_route_data(nc, route, r->start_time,
+						info->preferred_lifetime,
+						info->valid_lifetime,
+						gateway ? r->mtu : 0, true);
+		else if (route && !info->valid_lifetime)
+			netconfig_remove_icmp6_route(nc, route);
+	}
+
+	/*
+	 * Note: we may be emitting this before L_NETCONFIG_EVENT_CONFIGURE.
+	 * We should probably instead save the affected routes in separate
+	 * lists and add them to the _CONFIGURE event, suppressing any _UPDATE
+	 * events while nc->v6_configured is false.
+	 */
+	if (!l_queue_isempty(nc->routes.added) ||
+			!l_queue_isempty(nc->routes.updated) ||
+			!l_queue_isempty(nc->routes.removed))
+		netconfig_emit_event(nc, AF_INET6, L_NETCONFIG_EVENT_UPDATE);
+}
+
 LIB_EXPORT struct l_netconfig *l_netconfig_new(uint32_t ifindex)
 {
 	struct l_netconfig *nc;
@@ -382,6 +620,13 @@
 					netconfig_dhcp_event_handler,
 					nc, NULL);
 
+	nc->dhcp6_client = l_dhcp6_client_new(ifindex);
+
+	nc->icmp6_client = l_dhcp6_client_get_icmp6(nc->dhcp6_client);
+	l_icmp6_client_add_event_handler(nc->icmp6_client,
+					netconfig_icmp6_event_handler,
+					nc, NULL);
+
 	return nc;
 }
 
@@ -402,6 +647,7 @@
 	l_netconfig_set_domain_names_override(netconfig, AF_INET6, NULL);
 
 	l_dhcp_client_destroy(netconfig->dhcp_client);
+	l_dhcp6_client_destroy(netconfig->dhcp6_client);
 	l_netconfig_set_event_handler(netconfig, NULL, NULL, NULL);
 	l_queue_destroy(netconfig->addresses.current, NULL);
 	l_queue_destroy(netconfig->addresses.added, NULL);
@@ -735,19 +981,35 @@
 	if (!netconfig->v6_enabled)
 		goto done;
 
-	if (netconfig->v6_static_addr && !netconfig->do_static_work) {
+	if (netconfig->v6_static_addr) {
 		/*
 		 * We're basically ready to configure the interface
 		 * but do this in an idle callback.
 		 */
-		netconfig->do_static_work = l_idle_create(
+		if (!netconfig->do_static_work)
+			netconfig->do_static_work = l_idle_create(
 						netconfig_do_static_config,
 						netconfig, NULL);
+
+		goto done;
 	}
 
+	if (!l_dhcp6_client_start(netconfig->dhcp6_client))
+		goto stop_ipv4;
+
 done:
 	netconfig->started = true;
 	return true;
+
+stop_ipv4:
+	if (netconfig->v4_enabled) {
+		if (netconfig->v4_static_addr)
+			l_idle_remove(l_steal_ptr(netconfig->do_static_work));
+		else
+			l_dhcp_client_stop(netconfig->dhcp_client);
+	}
+
+	return false;
 }
 
 LIB_EXPORT void l_netconfig_stop(struct l_netconfig *netconfig)
@@ -771,6 +1033,7 @@
 	netconfig->v6_address = NULL;
 
 	l_dhcp_client_stop(netconfig->dhcp_client);
+	l_dhcp6_client_stop(netconfig->dhcp6_client);
 }
 
 LIB_EXPORT struct l_dhcp_client *l_netconfig_get_dhcp_client(