From d48688cf66644bfb76352505d867316808c454d7 Mon Sep 17 00:00:00 2001
From: Jan Luebbe <jlu@pengutronix.de>
Date: Wed, 4 Mar 2020 21:56:10 +0100
Subject: [PATCH] gluon-radv-filterd: redirect IPv6 packets to correct router

When multiple routers are in the same local mesh and clients roam from one (A)
to the next (B), the change of global IP and default gateway are not
synchronized. This leads to packets with an address belonging to router A to be
sent via router B (or the other way around). Those packets are then dropped by
wireguard at the concentrator.

To avoid this, we let gluon-radv-filterd monitor router advertisements and keep
a list of neighbouring v6 networks. With this information, it can maintain a
set of ebtables DNAT rules to redirect the packets to the matching gateway.

Signed-off-by: Jan Luebbe <jluebbe@debian.org>
---
 .../lib/gluon/ebtables/400-radv-filterd       |   4 +
 .../src/gluon-radv-filterd.c                  | 133 +++++++++++++++---
 package/gluon-radv-filterd/src/respondd.c     |   1 +
 3 files changed, 119 insertions(+), 19 deletions(-)

diff --git a/package/gluon-radv-filterd/luasrc/lib/gluon/ebtables/400-radv-filterd b/package/gluon-radv-filterd/luasrc/lib/gluon/ebtables/400-radv-filterd
index 178084d41..4e4f247bc 100644
--- a/package/gluon-radv-filterd/luasrc/lib/gluon/ebtables/400-radv-filterd
+++ b/package/gluon-radv-filterd/luasrc/lib/gluon/ebtables/400-radv-filterd
@@ -1,3 +1,7 @@
 chain('RADV_FILTER', 'DROP')
 rule 'FORWARD -p IPv6 -i bat0 --ip6-protocol ipv6-icmp --ip6-icmp-type router-advertisement -j RADV_FILTER'
 rule 'RADV_FILTER -j ACCEPT'
+
+chain('REDIRECT', 'RETURN', 'nat')
+rule('PREROUTING -p IPv6 --logical-in br-client --ip6-destination 2000::/3 -j REDIRECT', 'nat')
+rule('OUTPUT -p IPv6 --logical-out br-client --ip6-destination 2000::/3 -j REDIRECT', 'nat')
diff --git a/package/gluon-radv-filterd/src/gluon-radv-filterd.c b/package/gluon-radv-filterd/src/gluon-radv-filterd.c
index 7a7777522..a0b363af0 100644
--- a/package/gluon-radv-filterd/src/gluon-radv-filterd.c
+++ b/package/gluon-radv-filterd/src/gluon-radv-filterd.c
@@ -2,6 +2,8 @@
 /* SPDX-FileCopyrightText: 2017 Sven Eckelmann <sven@narfation.org> */
 /* SPDX-License-Identifier: BSD-2-Clause */
 
+// #define DEBUG
+
 #include <errno.h>
 #include <signal.h>
 #include <stdarg.h>
@@ -30,6 +32,8 @@
 #include <netinet/in.h>
 #include <netinet/ip6.h>
 
+#include <arpa/inet.h>
+
 #include <netlink/netlink.h>
 #include <netlink/genl/genl.h>
 #include <netlink/genl/ctrl.h>
@@ -87,6 +91,9 @@ struct router {
 	struct timespec eol;
 	struct ether_addr originator;
 	uint16_t tq;
+	bool redirected;
+	struct in6_addr lladdr;
+	struct in6_addr prefix;
 };
 
 static struct global {
@@ -156,6 +163,10 @@ static void cleanup(void) {
 		if (fork_execvp_timeout(&timeout, "ebtables-tiny", (const char *[])
 				{ "ebtables-tiny", "-A", G.chain, "-j", "ACCEPT", NULL }))
 			DEBUG_MSG("warning: adding new rule to ebtables chain %s failed", G.chain);
+
+		if (fork_execvp_timeout(&timeout, "ebtables-tiny", (const char *[])
+				{ "ebtables-tiny", "-t", "nat", "-F", "REDIRECT", NULL}))
+			DEBUG_MSG("warning: flushing ebtables nat chain REDIRECT failed", G.chain);
 	}
 }
 
@@ -232,7 +243,7 @@ static int init_packet_socket(unsigned int ifindex) {
 
 	struct sockaddr_ll bind_iface = {
 		.sll_family = AF_PACKET,
-		.sll_protocol = htons(ETH_P_IPV6),
+		.sll_protocol = htons(ETH_P_ALL), /* seems needed to recieve packets on bat0 */
 		.sll_ifindex = ifindex,
 	};
 	ret = bind(sock, (struct sockaddr *)&bind_iface, sizeof(bind_iface));
@@ -317,40 +328,85 @@ static struct router *router_add(const struct ether_addr *mac) {
 	return router;
 }
 
-static void router_update(const struct ether_addr *mac, uint16_t timeout) {
-	struct router *router;
-
-	router = router_find_src(mac);
-	if (!router)
-		router = router_add(mac);
-	if (!router)
-		return;
-
-	clock_gettime(CLOCK_MONOTONIC, &router->eol);
-	router->eol.tv_sec += timeout;
-}
-
 static void handle_ra(int sock) {
 	struct sockaddr_ll src;
 	struct ether_addr mac;
 	socklen_t addr_size = sizeof(src);
 	ssize_t len;
+	uint8_t *ptr;
 	struct {
-		struct ip6_hdr ip6;
-		struct nd_router_advert ra;
+		struct {
+			struct ip6_hdr ip6;
+			struct nd_router_advert ra;
+		} hdr;
+		uint8_t options[128];
 	} pkt;
+	struct router *router;
+	char addr_str[INET6_ADDRSTRLEN];
 
 	len = recvfrom(sock, &pkt, sizeof(pkt), 0, (struct sockaddr *)&src, &addr_size);
 	CHECK(len >= 0);
 
 	// BPF already checked that this is an ICMPv6 RA of a default router
-	CHECK((size_t)len >= sizeof(pkt));
-	CHECK(ntohs(pkt.ip6.ip6_plen) + sizeof(struct ip6_hdr) >= sizeof(pkt));
+	CHECK((size_t)len >= sizeof(pkt.hdr));
+	CHECK(ntohs(pkt.hdr.ip6.ip6_plen) + sizeof(struct ip6_hdr) >= sizeof(pkt.hdr));
 
 	memcpy(&mac, src.sll_addr, sizeof(mac));
 	DEBUG_MSG("received valid RA from " F_MAC, F_MAC_VAR(mac));
 
-	router_update(&mac, ntohs(pkt.ra.nd_ra_router_lifetime));
+	router = router_find_src(&mac);
+	if (!router)
+		router = router_add(&mac);
+	if (!router)
+		return;
+
+	clock_gettime(CLOCK_MONOTONIC, &router->eol);
+	router->eol.tv_sec += ntohs(pkt.hdr.ra.nd_ra_router_lifetime);
+
+	memcpy(&router->lladdr, &pkt.hdr.ip6.ip6_src, sizeof(router->lladdr));
+
+	DEBUG_MSG("%d bytes in packet", len);
+
+	// find prefix option
+	len -= sizeof(pkt.hdr);
+	ptr = (uint8_t*)&pkt + sizeof(pkt.hdr);
+	
+	while (len >= 8) {
+		unsigned int o_type = ptr[0];
+		unsigned int o_len = (unsigned int)ptr[1] << 3;
+		struct nd_opt_prefix_info *o_pi;
+
+		if (o_type != 3) {
+			ptr += o_len;
+			len -= o_len;
+			DEBUG_MSG("skipping option %d (size %d)", o_type, o_len);
+			continue;
+		}
+		CHECK(len >= o_len);
+		DEBUG_MSG("found option option %d (size %d)", o_type, o_len);
+
+		o_pi = (struct nd_opt_prefix_info*)ptr;
+		memcpy(&router->prefix, &o_pi->nd_opt_pi_prefix, sizeof(router->prefix));
+
+
+		ptr += o_len;
+		len -= o_len;
+		break;
+	}
+
+	DEBUG_MSG("%d bytes remaining", len);
+
+	if (inet_ntop(AF_INET6, &router->lladdr, addr_str, sizeof(addr_str))) {
+		DEBUG_MSG("lladdr: %s", addr_str);
+	} else {
+		DEBUG_MSG("lladdr: error");
+	}
+
+	if (inet_ntop(AF_INET6, &router->prefix, addr_str, sizeof(addr_str))) {
+		DEBUG_MSG("prefix: %s", addr_str);
+	} else {
+		DEBUG_MSG("prefix: error");
+	}
 
 check_failed:
 	return;
@@ -595,6 +651,43 @@ static void update_tqs(void) {
 	}
 }
 
+static void update_redirect(void) {
+	struct router *router;
+	struct timespec timeout = {
+		.tv_nsec = EBTABLES_TIMEOUT,
+	};
+
+	foreach(router, G.routers) {
+		char mac[F_MAC_LEN + 1];
+		char addr[INET6_ADDRSTRLEN];
+		char prefix[INET6_ADDRSTRLEN];
+
+		if (router->redirected)
+		    continue;
+		router->redirected = true;
+
+		snprintf(mac, sizeof(mac), F_MAC, F_MAC_VAR(router->src));
+
+		if (inet_ntop(AF_INET6, &router->prefix, addr, sizeof(addr)) == NULL) {
+			error_message(0, 0, "warning: failed to format prefix");
+			continue;
+		}
+		snprintf(prefix, sizeof(prefix), "%s/64", addr);
+
+		if (fork_execvp_timeout(&timeout, "ebtables-tiny", (const char *[])
+			{ "ebtables-tiny", "-t", "nat", "-A", "REDIRECT",
+			"-p", "IPv6",
+			"--ip6-source", prefix,
+			"--ip6-destination", "!", prefix,
+			"-d", "!",  mac,
+			"-j", "dnat",
+			"--to-destination", mac,
+			NULL }))
+		error_message(0, 0, "warning: adding new rule to ebtables chain REDIRECT failed");
+	}
+}
+
+
 static int fork_execvp_timeout(struct timespec *timeout, const char *file, const char *const argv[]) {
 	int ret;
 	pid_t child;
@@ -770,6 +863,8 @@ int main(int argc, char *argv[]) {
 				timespec_diff(&now, &next_update, &diff)) {
 			expire_routers();
 
+			update_redirect();
+
 			// all routers could have expired, check again
 			if (G.routers != NULL) {
 				if(timespec_diff(&now, &next_invalidation, &diff)) {
diff --git a/package/gluon-radv-filterd/src/respondd.c b/package/gluon-radv-filterd/src/respondd.c
index 8c2c7eb42..bfa9257ae 100644
--- a/package/gluon-radv-filterd/src/respondd.c
+++ b/package/gluon-radv-filterd/src/respondd.c
@@ -4,6 +4,7 @@
 #include <libgluonutil.h>
 #include <net/ethernet.h>
 #include <stdio.h>
+#include <string.h>
 
 #include "mac.h"
 
-- 
GitLab