diff --git a/docs/index.rst b/docs/index.rst
index be715325c82cf12025cc4e383bec0543b761b634..5fe481c0cb70c07ff5ebe8c0cee8792a03137fde 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -60,6 +60,7 @@ Several Freifunk communities in Germany use Gluon as the foundation of their Fre
    package/gluon-ebtables-filter-ra-dhcp
    package/gluon-ebtables-segment-mld
    package/gluon-ebtables-source-filter
+   package/gluon-radv-filterd
    package/gluon-web-admin
    package/gluon-web-logging
 
diff --git a/docs/package/gluon-radv-filterd.rst b/docs/package/gluon-radv-filterd.rst
new file mode 100644
index 0000000000000000000000000000000000000000..889496117afc79e45e5d9fbb2cbf4889f9aaebc9
--- /dev/null
+++ b/docs/package/gluon-radv-filterd.rst
@@ -0,0 +1,61 @@
+gluon-radv-filterd
+==================
+
+This package drops all incoming router advertisements except for the
+default router with the best metric according to B.A.T.M.A.N. advanced.
+
+Note that advertisements originating from the node itself (for example
+via gluon-radvd) are not affected and considered at all.
+
+Selected router
+---------------
+
+The router selection mechanism is independent from the batman-adv gateway mode.
+In contrast, the device originating the router advertisment could be any router
+or client connected to the mesh, as radv-filterd captures all router
+advertisements originating  from it. All nodes announcing router advertisement
+**with** a default lifetime greater than 0 are being considered as candidates.
+
+In case a router is not a batman-adv originator itself, its TQ is defined by
+the originator it is connected to. This lookup uses the batman-adv global
+translation table.
+
+Initially the router is the selected by choosing the candidate with the
+strongest TQ. When another candidate can provide a better TQ metric it is not
+picked up as the selected router until it will outperform the currently
+selected router by X metric units. The hysteresis threshold is configurable
+and prevents excessive flapping of the gateway.
+
+"Local" routers
+---------------
+
+The package has functionality to select "local" routers, i.e. those connected
+via cable or WLAN instead of via the mesh (technically: appearing in the
+``transtable_local``), a fake TQ of 512 so that they are always preferred.
+However, if used together with the :doc:`gluon-ebtables-filter-ra-dhcp`
+package, these router advertisements are filtered anyway and reach neither the
+node nor any other client. You currently have to disable the package or insert
+custom ebtables rules in order to use local routers.
+
+respondd module
+---------------
+
+This package also contains a module for respondd that announces the currently
+selected router via the ``statistics.gateway6`` property using its interface MAC
+address. Note that this is different from the ``statistics.gateway`` property,
+which contains the MAC address of the main B.A.T.M.A.N. adv slave interface of
+the selected IPv4 gateway.
+
+site.conf
+---------
+
+radv_filterd.threshold : optional
+    - minimal difference in TQ value that another gateway has to be better than
+      the currently chosen gateway to become the new chosen gateway
+    - defaults to ``20``
+
+Example::
+
+  radv_filterd = {
+    threshold = 20,
+  }
diff --git a/package/gluon-radv-filterd/Makefile b/package/gluon-radv-filterd/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..4dd2ebbac3ad7ec23e021a7b806e86c587b78ead
--- /dev/null
+++ b/package/gluon-radv-filterd/Makefile
@@ -0,0 +1,42 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-radv-filterd
+PKG_VERSION:=1
+PKG_RELEASE:=1
+
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+
+include ../gluon.mk
+
+define Package/gluon-radv-filterd
+  SECTION:=gluon
+  CATEGORY:=Gluon
+  TITLE:=Filter IPv6 router advertisements
+  DEPENDS:=+gluon-ebtables +libgluonutil +libbatadv +libnl-tiny
+endef
+
+MAKE_VARS += \
+        LIBNL_NAME="libnl-tiny" \
+        LIBNL_GENL_NAME="libnl-tiny"
+
+define Build/Prepare
+	mkdir -p $(PKG_BUILD_DIR)
+	$(CP) ./src/* $(PKG_BUILD_DIR)/
+endef
+
+define Package/gluon-radv-filterd/install
+	$(CP) ./files/* $(1)/
+
+	$(INSTALL_DIR) $(1)/usr/sbin/
+	$(INSTALL_BIN) $(PKG_BUILD_DIR)/gluon-radv-filterd $(1)/usr/sbin/
+
+	$(INSTALL_DIR) $(1)/lib/gluon/respondd
+	$(CP) $(PKG_BUILD_DIR)/respondd.so $(1)/lib/gluon/respondd/radv-filterd.so
+endef
+
+define Package/gluon-radv-filterd/postinst
+#!/bin/sh
+$(call GluonCheckSite,check_site.lua)
+endef
+
+$(eval $(call BuildPackage,gluon-radv-filterd))
diff --git a/package/gluon-radv-filterd/check_site.lua b/package/gluon-radv-filterd/check_site.lua
new file mode 100644
index 0000000000000000000000000000000000000000..fa38475db4b4407977576e76d1856883b5509708
--- /dev/null
+++ b/package/gluon-radv-filterd/check_site.lua
@@ -0,0 +1 @@
+need_number({'radv_filterd', 'threshold'}, false)
diff --git a/package/gluon-radv-filterd/files/etc/init.d/gluon-radv-filterd b/package/gluon-radv-filterd/files/etc/init.d/gluon-radv-filterd
new file mode 100755
index 0000000000000000000000000000000000000000..04906e560dbde850e0ba5e2894c66c878db2b6b7
--- /dev/null
+++ b/package/gluon-radv-filterd/files/etc/init.d/gluon-radv-filterd
@@ -0,0 +1,22 @@
+#!/bin/sh /etc/rc.common
+
+USE_PROCD=1
+START=50
+DAEMON=/usr/sbin/gluon-radv-filterd
+
+start_service() {
+	local threshold="$(lua -e 'print(require("gluon.site").radv_filterd.threshold(20))')"
+
+	procd_open_instance
+	procd_set_param command $DAEMON -i br-client -c RADV_FILTER -t $threshold
+	procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
+	procd_set_param netdev br-client
+	procd_set_param stderr 1
+	procd_close_instance
+}
+
+service_triggers() {
+	procd_open_trigger
+	procd_add_raw_trigger "interface.*" 1000 /etc/init.d/gluon-radv-filterd reload
+	procd_close_trigger
+}
diff --git a/package/gluon-radv-filterd/files/lib/gluon/ebtables/400-radv-filterd b/package/gluon-radv-filterd/files/lib/gluon/ebtables/400-radv-filterd
new file mode 100644
index 0000000000000000000000000000000000000000..178084d41f83fed9fa8167c004b24287fdd712ea
--- /dev/null
+++ b/package/gluon-radv-filterd/files/lib/gluon/ebtables/400-radv-filterd
@@ -0,0 +1,3 @@
+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'
diff --git a/package/gluon-radv-filterd/src/Makefile b/package/gluon-radv-filterd/src/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..17b655848fc1456dd12c8ffa8d18e147de63c2bd
--- /dev/null
+++ b/package/gluon-radv-filterd/src/Makefile
@@ -0,0 +1,49 @@
+all: gluon-radv-filterd respondd.so
+
+CPPFLAGS += -D_GNU_SOURCE
+
+ifeq ($(origin PKG_CONFIG), undefined)
+  PKG_CONFIG = pkg-config
+  ifeq ($(shell which $(PKG_CONFIG) 2>/dev/null),)
+    $(error $(PKG_CONFIG) not found)
+  endif
+endif
+
+ifeq ($(origin LIBNL_CFLAGS) $(origin LIBNL_LDLIBS), undefined undefined)
+  LIBNL_NAME ?= libnl-3.0
+  ifeq ($(shell $(PKG_CONFIG) --modversion $(LIBNL_NAME) 2>/dev/null),)
+    $(error No $(LIBNL_NAME) development libraries found!)
+  endif
+  LIBNL_CFLAGS += $(shell $(PKG_CONFIG) --cflags $(LIBNL_NAME))
+  LIBNL_LDLIBS +=  $(shell $(PKG_CONFIG) --libs $(LIBNL_NAME))
+endif
+CFLAGS += $(LIBNL_CFLAGS)
+LDLIBS += $(LIBNL_LDLIBS)
+
+ifeq ($(origin LIBNL_GENL_CFLAGS) $(origin LIBNL_GENL_LDLIBS), undefined undefined)
+  LIBNL_GENL_NAME ?= libnl-genl-3.0
+  ifeq ($(shell $(PKG_CONFIG) --modversion $(LIBNL_GENL_NAME) 2>/dev/null),)
+    $(error No $(LIBNL_GENL_NAME) development libraries found!)
+  endif
+  LIBNL_GENL_CFLAGS += $(shell $(PKG_CONFIG) --cflags $(LIBNL_GENL_NAME))
+  LIBNL_GENL_LDLIBS += $(shell $(PKG_CONFIG) --libs $(LIBNL_GENL_NAME))
+endif
+CFLAGS += $(LIBNL_GENL_CFLAGS)
+LDLIBS += $(LIBNL_GENL_LDLIBS)
+
+ifeq ($(origin LIBBATADV_CFLAGS) $(origin LIBBATADV_LDLIBS), undefined undefined)
+  LIBBATADV_NAME ?= libbatadv
+  ifeq ($(shell $(PKG_CONFIG) --modversion $(LIBBATADV_NAME) 2>/dev/null),)
+    $(error No $(LIBBATADV_NAME) development libraries found!)
+  endif
+  LIBBATADV_CFLAGS += $(shell $(PKG_CONFIG) --cflags $(LIBBATADV_NAME))
+  LIBBATADV_LDLIBS += $(shell $(PKG_CONFIG) --libs $(LIBBATADV_NAME))
+endif
+CFLAGS += $(LIBBATADV_CFLAGS)
+LDLIBS += $(LIBBATADV_LDLIBS)
+
+gluon-radv-filterd: gluon-radv-filterd.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS)
+
+respondd.so: respondd.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -shared -fPIC -o $@ $^ $(LDLIBS) -lgluonutil
diff --git a/package/gluon-radv-filterd/src/gluon-radv-filterd.c b/package/gluon-radv-filterd/src/gluon-radv-filterd.c
new file mode 100644
index 0000000000000000000000000000000000000000..990885ea889196b1a0df414382a871066bf74e20
--- /dev/null
+++ b/package/gluon-radv-filterd/src/gluon-radv-filterd.c
@@ -0,0 +1,796 @@
+/*
+   Copyright (c) 2016 Jan-Philipp Litza <janphilipp@litza.de>
+   Copyright (c) 2017 Sven Eckelmann <sven@narfation.org>
+   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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <errno.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/socket.h>
+#include <sys/select.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include <linux/filter.h>
+#include <linux/if_packet.h>
+#include <linux/limits.h>
+
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip6.h>
+
+#include <netlink/netlink.h>
+#include <netlink/genl/genl.h>
+#include <netlink/genl/ctrl.h>
+#include <batadv-genl.h>
+
+#include "mac.h"
+
+// Recheck TQs after this time even if no RA was received
+#define MAX_INTERVAL 60
+
+// Recheck TQs at most this often, even if new RAs were received (they won't
+// become the preferred routers until the TQs have been rechecked)
+// Also, the first update will take at least this long
+#define MIN_INTERVAL 15
+
+// max execution time of a single ebtables call in nanoseconds
+#define EBTABLES_TIMEOUT 500000000 // 500ms
+
+// TQ value assigned to local routers
+#define LOCAL_TQ 512
+
+#define BUFSIZE 1500
+
+#ifdef DEBUG
+#define CHECK(stmt) \
+    if(!(stmt)) { \
+        fprintf(stderr, "check failed: " #stmt "\n"); \
+        goto check_failed; \
+    }
+#define DEBUG_MSG(msg, ...) fprintf(stderr, msg "\n", ##__VA_ARGS__)
+#else
+#define CHECK(stmt) if(!(stmt)) goto check_failed;
+#define DEBUG_MSG(msg, ...) do {} while(0)
+#endif
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(A) (sizeof(A)/sizeof(A[0]))
+#endif
+
+#define foreach(item, list) \
+	for((item) = (list); (item) != NULL; (item) = (item)->next)
+
+#define foreach_safe(item, safe, list) \
+	for ((item) = (list); \
+	     (item) && (((safe) = item->next) || 1); \
+	     (item) = (safe))
+
+struct router {
+	struct router *next;
+	struct ether_addr src;
+	struct timespec eol;
+	struct ether_addr originator;
+	uint16_t tq;
+};
+
+static struct global {
+	int sock;
+	struct router *routers;
+	const char *mesh_iface;
+	const char *chain;
+	uint16_t max_tq;
+	uint16_t hysteresis_thresh;
+	struct router *best_router;
+	volatile sig_atomic_t stop_daemon;
+} G = {
+	.mesh_iface = "bat0",
+};
+
+static int fork_execvp_timeout(struct timespec *timeout, const char *file,
+			       const char *const argv[]);
+
+static void error_message(int status, int errnum, char *message, ...) {
+	va_list ap;
+	va_start(ap, message);
+	fflush(stdout);
+	vfprintf(stderr, message, ap);
+	va_end(ap);
+
+	if (errnum)
+		fprintf(stderr, ": %s", strerror(errnum));
+	fprintf(stderr, "\n");
+	if (status)
+		exit(status);
+}
+
+static int timespec_diff(struct timespec *tv1, struct timespec *tv2,
+			 struct timespec *tvdiff)
+{
+	tvdiff->tv_sec = tv1->tv_sec - tv2->tv_sec;
+	if (tv1->tv_nsec < tv2->tv_nsec) {
+		tvdiff->tv_nsec = 1000000000 + tv1->tv_nsec - tv2->tv_nsec;
+		tvdiff->tv_sec -= 1;
+	} else {
+		tvdiff->tv_nsec = tv1->tv_nsec - tv2->tv_nsec;
+	}
+
+	return (tvdiff->tv_sec >= 0);
+}
+
+static void cleanup(void) {
+	struct router *router;
+	struct timespec timeout = {
+		.tv_nsec = EBTABLES_TIMEOUT,
+	};
+
+	close(G.sock);
+
+	while (G.routers != NULL) {
+		router = G.routers;
+		G.routers = router->next;
+		free(router);
+	}
+
+	if (G.chain) {
+		/* Reset chain to accept everything again */
+		if (fork_execvp_timeout(&timeout, "ebtables", (const char *[])
+				{ "ebtables", "--concurrent", "-F", G.chain, NULL }))
+			DEBUG_MSG("warning: flushing ebtables chain %s failed, not adding a new rule", G.chain);
+
+		if (fork_execvp_timeout(&timeout, "ebtables", (const char *[])
+				{ "ebtables", "--concurrent", "-A", G.chain, "-j", "ACCEPT", NULL }))
+			DEBUG_MSG("warning: adding new rule to ebtables chain %s failed", G.chain);
+	}
+}
+
+static void usage(const char *msg) {
+	if (msg != NULL && *msg != '\0') {
+		fprintf(stderr, "ERROR: %s\n\n", msg);
+	}
+	fprintf(stderr,
+		"Usage: %s [-m <mesh_iface>] [-t <thresh>] -c <chain> -i <iface>\n\n"
+		"  -m <mesh_iface>  B.A.T.M.A.N. advanced mesh interface used to get metric\n"
+		"                   information (\"TQ\") for the available gateways. Default: bat0\n"
+		"  -t <thresh>      Minimum TQ difference required to switch the gateway.\n"
+		"                   Default: 0\n"
+		"  -c <chain>       ebtables chain that should be managed by the daemon. The\n"
+		"                   chain already has to exist on program invocation and should\n"
+		"                   have a DROP policy. It will be flushed by the program!\n"
+		"  -i <iface>       Interface to listen on for router advertisements. Should be\n"
+		"                   <mesh_iface> or a bridge on top of it, as no metric\n"
+		"                   information will be available for hosts on other interfaces.\n\n",
+		program_invocation_short_name);
+	cleanup();
+	if (msg == NULL)
+		exit(EXIT_SUCCESS);
+	else
+		exit(EXIT_FAILURE);
+}
+
+#define exit_errmsg(message, ...) { \
+	fprintf(stderr, message "\n", ##__VA_ARGS__); \
+	cleanup(); \
+	exit(1); \
+	}
+
+static inline void exit_errno(const char *message) {
+	cleanup();
+	error_message(1, errno, "error: %s", message);
+}
+
+static inline void warn_errno(const char *message) {
+	error_message(0, errno, "warning: %s", message);
+}
+
+static int init_packet_socket(unsigned int ifindex) {
+	struct sock_filter radv_filter_code[] = {
+		// check that this is an ICMPv6 packet
+		BPF_STMT(BPF_LD|BPF_B|BPF_ABS, offsetof(struct ip6_hdr, ip6_nxt)),
+		BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, IPPROTO_ICMPV6, 0, 7),
+		// check that this is a router advertisement
+		BPF_STMT(BPF_LD|BPF_B|BPF_ABS, sizeof(struct ip6_hdr) + offsetof(struct icmp6_hdr, icmp6_type)),
+		BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ND_ROUTER_ADVERT, 0, 5),
+		// check that the code field in the ICMPv6 header is 0
+		BPF_STMT(BPF_LD|BPF_B|BPF_ABS, sizeof(struct ip6_hdr) + offsetof(struct nd_router_advert, nd_ra_code)),
+		BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, 0, 0, 3),
+		// check that this is a default route (lifetime > 0)
+		BPF_STMT(BPF_LD|BPF_H|BPF_ABS, sizeof(struct ip6_hdr) + offsetof(struct nd_router_advert, nd_ra_router_lifetime)),
+		BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, 0, 1, 0),
+		// return true
+		BPF_STMT(BPF_RET|BPF_K, 0xffffffff),
+		// return false
+		BPF_STMT(BPF_RET|BPF_K, 0),
+	};
+
+	struct sock_fprog radv_filter = {
+	    .len = ARRAY_SIZE(radv_filter_code),
+	    .filter = radv_filter_code,
+	};
+
+	int sock = socket(AF_PACKET, SOCK_DGRAM|SOCK_CLOEXEC, htons(ETH_P_IPV6));
+	if (sock < 0)
+		exit_errno("can't open packet socket");
+	int ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &radv_filter, sizeof(radv_filter));
+	if (ret < 0)
+		exit_errno("can't attach socket filter");
+
+	struct sockaddr_ll bind_iface = {
+		.sll_family = AF_PACKET,
+		.sll_protocol = htons(ETH_P_IPV6),
+		.sll_ifindex = ifindex,
+	};
+	ret = bind(sock, (struct sockaddr *)&bind_iface, sizeof(bind_iface));
+	if (ret < 0)
+		exit_errno("can't bind socket");
+
+	return sock;
+}
+
+static void parse_cmdline(int argc, char *argv[]) {
+	int c;
+	unsigned int ifindex;
+	unsigned long int threshold;
+	char *endptr;
+	while ((c = getopt(argc, argv, "c:hi:m:t:")) != -1) {
+		switch (c) {
+			case 'i':
+				if (G.sock >= 0)
+					usage("-i given more than once");
+				ifindex = if_nametoindex(optarg);
+				if (ifindex == 0)
+					exit_errmsg("Unknown interface: %s", optarg);
+				G.sock = init_packet_socket(ifindex);
+				break;
+			case 'm':
+				G.mesh_iface = optarg;
+				break;
+			case 'c':
+				G.chain = optarg;
+				break;
+			case 't':
+				threshold = strtoul(optarg, &endptr, 10);
+				if (*endptr != '\0')
+					exit_errmsg("Threshold must be a number: %s", optarg);
+				if (threshold >= LOCAL_TQ)
+					exit_errmsg("Threshold too large: %ld (max is %d)", threshold, LOCAL_TQ);
+				G.hysteresis_thresh = (uint16_t) threshold;
+				break;
+			case 'h':
+				usage(NULL);
+				break;
+			default:
+				usage("");
+				break;
+		}
+	}
+}
+
+static struct router *router_find_src(const struct ether_addr *src) {
+	struct router *router;
+
+	foreach(router, G.routers) {
+		if (ether_addr_equal(router->src, *src))
+			return router;
+	}
+
+	return NULL;
+}
+
+static struct router *router_find_orig(const struct ether_addr *orig) {
+	struct router *router;
+
+	foreach(router, G.routers) {
+		if (ether_addr_equal(router->originator, *orig))
+			return router;
+	}
+
+	return NULL;
+}
+
+static struct router *router_add(const struct ether_addr *mac) {
+	struct router *router;
+
+	router = malloc(sizeof(*router));
+	if (!router)
+		return NULL;
+
+	router->src = *mac;
+	router->next = G.routers;
+	G.routers = router;
+	router->eol.tv_sec = 0;
+	router->eol.tv_nsec = 0;
+	memset(&router->originator, 0, sizeof(router->originator));
+
+	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;
+	struct {
+		struct ip6_hdr ip6;
+		struct nd_router_advert ra;
+	} pkt;
+
+	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));
+
+	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));
+
+check_failed:
+	return;
+}
+
+static void expire_routers(void) {
+	struct router **prev_ptr = &G.routers;
+	struct router *router;
+	struct router *safe;
+	struct timespec now;
+	struct timespec diff;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+
+	foreach_safe(router, safe, G.routers) {
+		if (timespec_diff(&now, &router->eol, &diff)) {
+			DEBUG_MSG("router " F_MAC " expired", F_MAC_VAR(router->src));
+			*prev_ptr = router->next;
+			if (G.best_router == router)
+				G.best_router = NULL;
+			free(router);
+		} else {
+			prev_ptr = &router->next;
+		}
+	}
+}
+
+static int parse_tt_global(struct nl_msg *msg,
+			   void *arg __attribute__((unused)))
+{
+	static const enum batadv_nl_attrs mandatory[] = {
+		BATADV_ATTR_TT_ADDRESS,
+		BATADV_ATTR_ORIG_ADDRESS,
+	};
+	struct nlattr *attrs[BATADV_ATTR_MAX + 1];
+	struct nlmsghdr *nlh = nlmsg_hdr(msg);
+	struct ether_addr mac_a, mac_b;
+	struct genlmsghdr *ghdr;
+	struct router *router;
+	uint8_t *addr;
+	uint8_t *orig;
+
+	// parse netlink entry
+	if (!genlmsg_valid_hdr(nlh, 0))
+		return NL_OK;
+
+	ghdr = nlmsg_data(nlh);
+
+	if (ghdr->cmd != BATADV_CMD_GET_TRANSTABLE_GLOBAL)
+		return NL_OK;
+
+	if (nla_parse(attrs, BATADV_ATTR_MAX, genlmsg_attrdata(ghdr, 0),
+		      genlmsg_len(ghdr), batadv_genl_policy)) {
+		return NL_OK;
+	}
+
+	if (batadv_genl_missing_attrs(attrs, mandatory, ARRAY_SIZE(mandatory)))
+		return NL_OK;
+
+	addr = nla_data(attrs[BATADV_ATTR_TT_ADDRESS]);
+	orig = nla_data(attrs[BATADV_ATTR_ORIG_ADDRESS]);
+
+	if (!attrs[BATADV_ATTR_FLAG_BEST])
+		return NL_OK;
+
+	MAC2ETHER(mac_a, addr);
+	MAC2ETHER(mac_b, orig);
+
+	// update router
+	router = router_find_src(&mac_a);
+	if (!router)
+		return NL_OK;
+
+	DEBUG_MSG("Found originator for " F_MAC ", it's " F_MAC,
+		  F_MAC_VAR(router->src), F_MAC_VAR(mac_b));
+	router->originator = mac_b;
+
+	return NL_OK;
+}
+
+static int parse_originator(struct nl_msg *msg,
+			    void *arg __attribute__((unused)))
+{
+
+	static const enum batadv_nl_attrs mandatory[] = {
+		BATADV_ATTR_ORIG_ADDRESS,
+		BATADV_ATTR_TQ,
+	};
+	struct nlattr *attrs[BATADV_ATTR_MAX + 1];
+	struct nlmsghdr *nlh = nlmsg_hdr(msg);
+	struct ether_addr mac_a;
+	struct genlmsghdr *ghdr;
+	struct router *router;
+	uint8_t *orig;
+	uint8_t tq;
+
+	// parse netlink entry
+	if (!genlmsg_valid_hdr(nlh, 0))
+		return NL_OK;
+
+	ghdr = nlmsg_data(nlh);
+
+	if (ghdr->cmd != BATADV_CMD_GET_ORIGINATORS)
+		return NL_OK;
+
+	if (nla_parse(attrs, BATADV_ATTR_MAX, genlmsg_attrdata(ghdr, 0),
+		      genlmsg_len(ghdr), batadv_genl_policy)) {
+		return NL_OK;
+	}
+
+	if (batadv_genl_missing_attrs(attrs, mandatory, ARRAY_SIZE(mandatory)))
+		return NL_OK;
+
+	orig = nla_data(attrs[BATADV_ATTR_ORIG_ADDRESS]);
+	tq = nla_get_u8(attrs[BATADV_ATTR_TQ]);
+
+	if (!attrs[BATADV_ATTR_FLAG_BEST])
+		return NL_OK;
+
+	MAC2ETHER(mac_a, orig);
+
+	// update router
+	router = router_find_orig(&mac_a);
+	if (!router)
+		return NL_OK;
+
+	DEBUG_MSG("Found TQ for router " F_MAC " (originator " F_MAC "), it's %d",
+		  F_MAC_VAR(router->src), F_MAC_VAR(router->originator), tq);
+	router->tq = tq;
+	if (router->tq > G.max_tq)
+		G.max_tq = router->tq;
+
+	return NL_OK;
+}
+
+static int parse_tt_local(struct nl_msg *msg,
+			  void *arg __attribute__((unused)))
+{
+	static const enum batadv_nl_attrs mandatory[] = {
+		BATADV_ATTR_TT_ADDRESS,
+	};
+	struct nlattr *attrs[BATADV_ATTR_MAX + 1];
+	struct nlmsghdr *nlh = nlmsg_hdr(msg);
+	struct ether_addr mac_a;
+	struct genlmsghdr *ghdr;
+	struct router *router;
+	uint8_t *addr;
+
+	// parse netlink entry
+	if (!genlmsg_valid_hdr(nlh, 0))
+		return NL_OK;
+
+	ghdr = nlmsg_data(nlh);
+
+	if (ghdr->cmd != BATADV_CMD_GET_TRANSTABLE_LOCAL)
+		return NL_OK;
+
+	if (nla_parse(attrs, BATADV_ATTR_MAX, genlmsg_attrdata(ghdr, 0),
+		      genlmsg_len(ghdr), batadv_genl_policy)) {
+		return NL_OK;
+	}
+
+	if (batadv_genl_missing_attrs(attrs, mandatory, ARRAY_SIZE(mandatory)))
+		return NL_OK;
+
+	addr = nla_data(attrs[BATADV_ATTR_TT_ADDRESS]);
+	MAC2ETHER(mac_a, addr);
+
+	// update router
+	router = router_find_src(&mac_a);
+	if (!router)
+		return NL_OK;
+
+	DEBUG_MSG("Found router " F_MAC " in transtable_local, assigning TQ %d",
+		  F_MAC_VAR(router->src), LOCAL_TQ);
+	router->tq = LOCAL_TQ;
+	if (router->tq > G.max_tq)
+		G.max_tq = router->tq;
+
+	return NL_OK;
+}
+
+static void update_tqs(void) {
+	struct router *router;
+	bool update_originators = false;
+	struct ether_addr unspec;
+	struct batadv_nlquery_opts opts;
+	int ret;
+
+	// reset TQs
+	memset(&unspec, 0, sizeof(unspec));
+	foreach(router, G.routers) {
+		router->tq = 0;
+		if (ether_addr_equal(router->originator, unspec))
+			update_originators = true;
+	}
+
+	// translate all router's MAC addresses to originators simultaneously
+	if (update_originators) {
+		opts.err = 0;
+		ret = batadv_genl_query(G.mesh_iface,
+					BATADV_CMD_GET_TRANSTABLE_GLOBAL,
+					parse_tt_global, NLM_F_DUMP, &opts);
+		if (ret < 0)
+			fprintf(stderr, "Parsing of global translation table failed\n");
+	}
+
+	// look up TQs of originators
+	G.max_tq = 0;
+	opts.err = 0;
+	ret = batadv_genl_query(G.mesh_iface,
+				BATADV_CMD_GET_ORIGINATORS,
+				parse_originator, NLM_F_DUMP, &opts);
+	if (ret < 0)
+		fprintf(stderr, "Parsing of originators failed\n");
+
+	// if all routers have a TQ value, we don't need to check translocal
+	foreach(router, G.routers) {
+		if (router->tq == 0)
+			break;
+	}
+	if (router != NULL) {
+		opts.err = 0;
+		ret = batadv_genl_query(G.mesh_iface,
+					BATADV_CMD_GET_TRANSTABLE_LOCAL,
+					parse_tt_local, NLM_F_DUMP, &opts);
+		if (ret < 0)
+			fprintf(stderr, "Parsing of global translation table failed\n");
+	}
+
+	foreach(router, G.routers) {
+		if (router->tq == 0) {
+			if (ether_addr_equal(router->originator, unspec))
+				fprintf(stderr,
+					"Unable to find router " F_MAC " in transtable_{global,local}\n",
+					F_MAC_VAR(router->src));
+			else
+				fprintf(stderr,
+					"Unable to find TQ for originator " F_MAC " (router " F_MAC ")\n",
+					F_MAC_VAR(router->originator),
+					F_MAC_VAR(router->src));
+		}
+	}
+}
+
+static int fork_execvp_timeout(struct timespec *timeout, const char *file, const char *const argv[]) {
+	int ret;
+	pid_t child;
+	siginfo_t info;
+	sigset_t signals, oldsignals;
+	sigemptyset(&signals);
+	sigaddset(&signals, SIGCHLD);
+
+	sigprocmask(SIG_BLOCK, &signals, &oldsignals);
+	child = fork();
+	if (child == 0) {
+		sigprocmask(SIG_SETMASK, &oldsignals, NULL);
+		// casting discards const, but should be safe
+		// (see http://stackoverflow.com/q/36925388)
+		execvp(file, (char**) argv);
+		fprintf(stderr, "can't execvp(\"%s\", ...): %s\n", file, strerror(errno));
+		_exit(1);
+	}
+	else if (child < 0) {
+		perror("Failed to fork()");
+		return -1;
+	}
+
+	ret = sigtimedwait(&signals, &info, timeout);
+	sigprocmask(SIG_SETMASK, &oldsignals, NULL);
+
+	if (ret == SIGCHLD) {
+		if (info.si_pid != child) {
+			cleanup();
+			error_message(1, 0,
+				"BUG: We received a SIGCHLD from a child we didn't spawn (expected PID %d, got %d)",
+				child, info.si_pid);
+		}
+
+		waitpid(child, NULL, 0);
+
+		return info.si_status;
+	}
+
+	if (ret < 0 && errno == EAGAIN)
+		error_message(0, 0, "warning: child %d took too long, killing", child);
+	else if (ret < 0)
+		warn_errno("sigtimedwait failed, killing child");
+	else
+		error_message(1, 0,
+				"BUG: sigtimedwait() returned some other signal than SIGCHLD: %d",
+				ret);
+
+	kill(child, SIGKILL);
+	kill(child, SIGCONT);
+	waitpid(child, NULL, 0);
+	return -1;
+}
+
+static bool election_required(void)
+{
+	if (!G.best_router)
+		return true;
+
+	/* should never happen. G.max_tq also contains G.best_router->tq */
+	if (G.max_tq < G.best_router->tq)
+		return false;
+
+	if ((G.max_tq - G.best_router->tq) <= G.hysteresis_thresh)
+		return false;
+
+	return true;
+}
+
+static void update_ebtables(void) {
+	struct timespec timeout = {
+		.tv_nsec = EBTABLES_TIMEOUT,
+	};
+	char mac[F_MAC_LEN + 1];
+	struct router *router;
+
+	if (!election_required()) {
+		DEBUG_MSG(F_MAC " is still good enough with TQ=%d (max_tq=%d), not executing ebtables",
+			F_MAC_VAR(G.best_router->src),
+			G.best_router->tq,
+			G.max_tq);
+		return;
+	}
+
+	foreach(router, G.routers) {
+		if (router->tq == G.max_tq) {
+			snprintf(mac, sizeof(mac), F_MAC, F_MAC_VAR(router->src));
+			break;
+		}
+	}
+	if (G.best_router)
+		fprintf(stderr, "Switching from " F_MAC " (TQ=%d) to %s (TQ=%d)\n",
+			F_MAC_VAR(G.best_router->src),
+			G.best_router->tq,
+			mac,
+			G.max_tq);
+	else
+		fprintf(stderr, "Switching to %s (TQ=%d)\n",
+			mac,
+			G.max_tq);
+	G.best_router = router;
+
+	if (fork_execvp_timeout(&timeout, "ebtables", (const char *[])
+			{ "ebtables", "--concurrent", "-F", G.chain, NULL }))
+		error_message(0, 0, "warning: flushing ebtables chain %s failed, not adding a new rule", G.chain);
+	else if (fork_execvp_timeout(&timeout, "ebtables", (const char *[])
+			{ "ebtables", "--concurrent", "-A", G.chain, "-s", mac, "-j", "ACCEPT", NULL }))
+		error_message(0, 0, "warning: adding new rule to ebtables chain %s failed", G.chain);
+}
+
+static void sighandler(int sig __attribute__((unused)))
+{
+	G.stop_daemon = 1;
+}
+
+int main(int argc, char *argv[]) {
+	int retval;
+	fd_set rfds;
+	struct timeval tv;
+	struct timespec next_update;
+	struct timespec now;
+	struct timespec diff;
+
+	clock_gettime(CLOCK_MONOTONIC, &next_update);
+	next_update.tv_sec += MIN_INTERVAL;
+
+	G.sock = -1;
+	parse_cmdline(argc, argv);
+
+	if (G.sock < 0)
+		usage("No interface set!");
+
+	if (G.chain == NULL)
+		usage("No chain set!");
+
+	G.stop_daemon = 0;
+	signal(SIGINT, sighandler);
+	signal(SIGTERM, sighandler);
+
+	while (!G.stop_daemon) {
+		FD_ZERO(&rfds);
+		FD_SET(G.sock, &rfds);
+
+		tv.tv_sec = MAX_INTERVAL;
+		tv.tv_usec = 0;
+		retval = select(G.sock + 1, &rfds, NULL, NULL, &tv);
+
+		if (retval < 0) {
+			if (errno != EINTR)
+				exit_errno("select() failed");
+		} else if (retval) {
+			if (FD_ISSET(G.sock, &rfds)) {
+				handle_ra(G.sock);
+			}
+		}
+		else
+			DEBUG_MSG("select() timeout expired");
+
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		if (G.routers != NULL &&
+		    timespec_diff(&now, &next_update, &diff)) {
+			expire_routers();
+
+			// all routers could have expired, check again
+			if (G.routers != NULL) {
+				update_tqs();
+				update_ebtables();
+
+				next_update = now;
+				next_update.tv_sec += MIN_INTERVAL;
+			}
+		}
+	}
+
+	cleanup();
+	return 0;
+}
diff --git a/package/gluon-radv-filterd/src/mac.h b/package/gluon-radv-filterd/src/mac.h
new file mode 100644
index 0000000000000000000000000000000000000000..cc24d90779ffe4e97fb22ea43fa0f8054090f500
--- /dev/null
+++ b/package/gluon-radv-filterd/src/mac.h
@@ -0,0 +1,18 @@
+#include <stdint.h>
+#include <linux/if_ether.h>
+
+#define F_MAC "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx"
+#define F_MAC_LEN 17
+#define F_MAC_VAR(var) \
+	(var).ether_addr_octet[0], (var).ether_addr_octet[1], \
+	(var).ether_addr_octet[2], (var).ether_addr_octet[3], \
+	(var).ether_addr_octet[4], (var).ether_addr_octet[5]
+#define F_MAC_VAR_REF(var) \
+	&(var).ether_addr_octet[0], &(var).ether_addr_octet[1], \
+	&(var).ether_addr_octet[2], &(var).ether_addr_octet[3], \
+	&(var).ether_addr_octet[4], &(var).ether_addr_octet[5]
+#define MAC2ETHER(_ether, _mac) memcpy((_ether).ether_addr_octet, \
+				       (_mac), ETH_ALEN)
+
+#define ether_addr_equal(_a, _b) (memcmp((_a).ether_addr_octet, \
+					 (_b).ether_addr_octet, ETH_ALEN) == 0)
diff --git a/package/gluon-radv-filterd/src/respondd.c b/package/gluon-radv-filterd/src/respondd.c
new file mode 100644
index 0000000000000000000000000000000000000000..7e65a3d951d9f1210b26420f341f938087dba588
--- /dev/null
+++ b/package/gluon-radv-filterd/src/respondd.c
@@ -0,0 +1,49 @@
+#include <respondd.h>
+
+#include <json-c/json.h>
+#include <libgluonutil.h>
+#include <net/ethernet.h>
+#include <stdio.h>
+
+#include "mac.h"
+
+static struct json_object * get_radv_filter() {
+	FILE *f = popen("exec ebtables --concurrent -L RADV_FILTER", "r");
+	char *line = NULL;
+	size_t len = 0;
+	struct ether_addr mac = {};
+	struct ether_addr unspec = {};
+	char macstr[F_MAC_LEN + 1] = "";
+
+	if (!f)
+		return NULL;
+
+	while (getline(&line, &len, f) > 0) {
+		if (sscanf(line, "-s " F_MAC " -j ACCEPT\n", F_MAC_VAR_REF(mac)) == ETH_ALEN)
+			break;
+	}
+	free(line);
+
+	pclose(f);
+
+	memset(&unspec, 0, sizeof(unspec));
+	if (ether_addr_equal(mac, unspec)) {
+		return NULL;
+	} else {
+		snprintf(macstr, sizeof(macstr), F_MAC, F_MAC_VAR(mac));
+		return gluonutil_wrap_string(macstr);
+	}
+}
+
+static struct json_object * respondd_provider_statistics() {
+	struct json_object *ret = json_object_new_object();
+
+	json_object_object_add(ret, "gateway6", get_radv_filter());
+
+	return ret;
+}
+
+const struct respondd_provider_info respondd_providers[] = {
+	{"statistics", respondd_provider_statistics},
+	{}
+};