Browse Source

Add package 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.
Jan-Philipp Litza 7 years ago
parent
commit
1a52ff40b9

+ 46 - 0
package/gluon-radv-filterd/Makefile

@@ -0,0 +1,46 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-radv-filterd
+PKG_VERSION:=1
+PKG_RELEASE:=1
+
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/gluon-radv-filterd
+  SECTION:=gluon
+  CATEGORY:=Gluon
+  TITLE:=Filter IPv6 router advertisements
+  DEPENDS:=+gluon-ebtables
+endef
+
+define Package/gluon-radv-filterd/description
+	Gluon community wifi mesh firmware framework: filter IPv6 router advertisements
+endef
+
+define Build/Prepare
+	mkdir -p $(PKG_BUILD_DIR)
+	$(CP) ./src/* $(PKG_BUILD_DIR)/
+endef
+
+define Build/Configure
+endef
+
+define Build/Compile
+	CFLAGS="$(TARGET_CFLAGS)" CPPFLAGS="$(TARGET_CPPFLAGS)" $(MAKE) -C $(PKG_BUILD_DIR) $(TARGET_CONFIGURE_OPTS)
+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/
+endef
+
+define Package/gluon-radv-filterd/postinst
+#!/bin/sh
+$(call GluonCheckSite,check_site.lua)
+endef
+
+$(eval $(call BuildPackage,gluon-radv-filterd))

+ 28 - 0
package/gluon-radv-filterd/README.md

@@ -0,0 +1,28 @@
+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.
+
+"Best" router
+-------------
+The best router is determined by the TQ that is reported for its originator by
+B.A.T.M.A.N. advanced. If, for some reason, another gateway with a better TQ
+appears or an existing gateway increases its TQ above that of the chosen
+gateway, the chosen gateway will remain selected until the better gateway has a
+TQ value at least X higher than the selected gateway. This is called
+hysteresis, and X can be specified on the commandline/via UCI/the site.conf and
+defaults to 20 (just as for the IPv4 gateway selection feature built into
+B.A.T.M.A.N. advanced).
+
+"Local" routers
+---------------
+The package has functionality to assign "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 `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.

+ 3 - 0
package/gluon-radv-filterd/check_site.lua

@@ -0,0 +1,3 @@
+if need_table('radv_filterd', nil, false) then
+    need_number('radv_filterd.threshold')
+end

+ 4 - 0
package/gluon-radv-filterd/files/etc/config/gluon-radv-filterd

@@ -0,0 +1,4 @@
+config filterd
+	option iface 'br-client'
+	option chain 'RADV_FILTER'
+	option threshold '20'

+ 34 - 0
package/gluon-radv-filterd/files/etc/init.d/gluon-radv-filterd

@@ -0,0 +1,34 @@
+#!/bin/sh /etc/rc.common
+
+USE_PROCD=1
+START=50
+DAEMON=/usr/sbin/gluon-radv-filterd
+
+validate_filterd_section() {
+    uci_validate_section gluon-radv-filterd filterd "${1}" \
+            'iface:string' \
+            'chain:string:RADV_FILTER' \
+            'threshold:uinteger:20'
+}
+
+start_service() {
+    config_load gluon-radv-filterd
+    config_foreach start_filterd filterd
+}
+
+start_filterd() {
+    local iface chain threshold
+    validate_filterd_section "$1"
+
+    procd_open_instance
+    procd_set_param command $DAEMON -i "$iface" -c "$chain" -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_add_reload_trigger "gluon-radv-filterd"
+    procd_add_validation "validate_filterd_section"
+}

+ 3 - 0
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'

+ 11 - 0
package/gluon-radv-filterd/luasrc/lib/gluon/upgrade/300-gluon-radv-filterd

@@ -0,0 +1,11 @@
+#!/usr/bin/lua
+
+local site = require 'gluon.site_config'
+local uci = (require 'luci.model.uci').cursor()
+
+if site.radv_filterd and site.radv_filterd.threshold then
+    uci:foreach('gluon-radv-filterd', 'filterd', function(section)
+        uci:set('gluon-radv-filterd', section['.name'], 'threshold', site.radv_filterd.threshold)
+    end)
+    uci:save('gluon-radv-filterd')
+end

+ 4 - 0
package/gluon-radv-filterd/src/Makefile

@@ -0,0 +1,4 @@
+all: gluon-radv-filterd
+
+gluon-radv-filterd: gluon-radv-filterd.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -Wall -o $@ $^ $(LDLIBS)

+ 513 - 0
package/gluon-radv-filterd/src/gluon-radv-filterd.c

@@ -0,0 +1,513 @@
+#define _GNU_SOURCE
+#include <error.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdarg.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/if.h>
+
+#include <linux/filter.h>
+#include <linux/if_packet.h>
+#include <linux/if_ether.h>
+#include <linux/limits.h>
+
+#include <netinet/icmp6.h>
+#include <netinet/ip6.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 5
+
+// max execution time of a single ebtables call in nanoseconds
+#define EBTABLES_TIMEOUT 1e8 // 100ms
+
+// TQ value assigned to local routers
+#define LOCAL_TQ 512
+
+#define BUFSIZE 1500
+
+#define DEBUGFS "/sys/kernel/debug/batman_adv/%s/"
+#define ORIGINATORS DEBUGFS "originators"
+#define TRANSTABLE_GLOBAL DEBUGFS "transtable_global"
+#define TRANSTABLE_LOCAL DEBUGFS "transtable_local"
+
+#define F_MAC "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx"
+#define F_MAC_IGN "%*2x:%*2x:%*2x:%*2x:%*2x:%*2x"
+#define F_MAC_VAR(var) var[0], var[1], var[2], var[3], var[4], var[5]
+
+#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
+
+typedef uint8_t macaddr_t[ETH_ALEN];
+
+struct list_item {
+	struct list *next;
+};
+
+#define foreach(item, list) \
+	for(item = list; item != NULL; item = item->next)
+
+struct router {
+	struct router *next;
+	macaddr_t src;
+	time_t eol;
+	macaddr_t originator;
+	uint16_t tq;
+};
+
+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;
+} G = {
+	.mesh_iface = "bat0",
+};
+
+
+static void cleanup() {
+	struct router *router;
+	close(G.sock);
+
+	while (G.routers != NULL) {
+		router = G.routers;
+		G.routers = router->next;
+		free(router);
+	}
+}
+
+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(1, errno, "error: %s", message);
+}
+
+static inline void warn_errno(const char *message) {
+	error(0, errno, "warning: %s", message);
+}
+
+static int init_packet_socket(unsigned int ifindex) {
+	// generated by tcpdump -i tun "icmp6 and ip6[40] = 134" -dd
+	// Important: Generate on TUN interface (because the socket is SOCK_DGRAM)!
+	struct sock_filter radv_filter_code[] = {
+		{ 0x30, 0, 0, 0x00000000 },
+		{ 0x54, 0, 0, 0x000000f0 },
+		{ 0x15, 0, 8, 0x00000060 },
+		{ 0x30, 0, 0, 0x00000006 },
+		{ 0x15, 3, 0, 0x0000003a },
+		{ 0x15, 0, 5, 0x0000002c },
+		{ 0x30, 0, 0, 0x00000028 },
+		{ 0x15, 0, 3, 0x0000003a },
+		{ 0x30, 0, 0, 0x00000028 },
+		{ 0x15, 0, 1, 0x00000086 },
+		{ 0x06, 0, 0, 0x0000ffff },
+		{ 0x06, 0, 0, 0x00000000 },
+	};
+
+	struct sock_fprog radv_filter = {
+	    .len = ARRAY_SIZE(radv_filter_code),
+	    .filter = radv_filter_code,
+	};
+
+	int sock = socket(AF_PACKET, SOCK_DGRAM|SOCK_CLOEXEC, ETH_P_IPV6);
+	if (sock < 0)
+		exit_errno("can't open packet socket");
+	setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &radv_filter, sizeof(radv_filter));
+
+	struct sockaddr_ll bind_iface = {
+		.sll_family = AF_PACKET,
+		.sll_protocol = ETH_P_IPV6,
+		.sll_ifindex = ifindex,
+	};
+	bind(sock, (struct sockaddr *)&bind_iface, sizeof(bind_iface));
+
+	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 void handle_ra(int sock) {
+	struct sockaddr_ll src;
+	unsigned int addr_size = sizeof(src);
+	size_t len;
+	uint8_t buffer[BUFSIZE] __attribute__((aligned(8)));
+	struct ip6_hdr *pkt;
+	struct ip6_ext *ext;
+	struct nd_router_advert *ra;
+	uint8_t ext_type;
+
+	len = recvfrom(sock, buffer, BUFSIZE, 0, (struct sockaddr *)&src, &addr_size);
+
+	// skip IPv6 headers, ensuring packet is long enough
+	CHECK(len > sizeof(struct ip6_hdr));
+	pkt = (struct ip6_hdr *)buffer;
+	CHECK(len >= ntohs(pkt->ip6_plen) + sizeof(struct ip6_hdr));
+	ext_type = pkt->ip6_nxt;
+	ext = (void*)pkt + sizeof(struct ip6_hdr);
+	while (ext_type != IPPROTO_ICMPV6) {
+		CHECK((void*)ext < (void*)pkt + sizeof(struct ip6_hdr) + len);
+		CHECK(ext->ip6e_len > 0);
+		ext_type = ext->ip6e_nxt;
+		ext = (void*)ext + ext->ip6e_len;
+	}
+
+	// partially parse router advertisement
+	CHECK((void*)ext + sizeof(struct nd_router_advert) <= (void*)pkt + sizeof(struct ip6_hdr) + len);
+	ra = (struct nd_router_advert *) ext;
+	CHECK(ra->nd_ra_type == ND_ROUTER_ADVERT);
+	CHECK(ra->nd_ra_code == 0);
+	// we only want default routers
+	CHECK(ra->nd_ra_router_lifetime > 0);
+
+	DEBUG_MSG("received valid RA from " F_MAC, F_MAC_VAR(src.sll_addr));
+
+	// update list of known routers
+	struct router *router;
+	foreach(router, G.routers) {
+		if (!memcmp(router->src, src.sll_addr, sizeof(macaddr_t))) {
+			break;
+		}
+	}
+	if (!router) {
+		router = malloc(sizeof(struct router));
+		memcpy(router->src, src.sll_addr, 8);
+		router->next = G.routers;
+		G.routers = router;
+	}
+	router->eol = time(NULL) + ra->nd_ra_router_lifetime;
+
+check_failed:
+	return;
+}
+
+static void expire_routers() {
+	struct router **prev_ptr = &G.routers;
+	struct router *router;
+	time_t now = time(NULL);
+
+	foreach(router, G.routers) {
+		if (router->eol < now) {
+			DEBUG_MSG("router " F_MAC " expired", F_MAC_VAR(router->src));
+			*prev_ptr = router->next;
+			free(router);
+		} else {
+			prev_ptr = &router->next;
+		}
+	}
+}
+
+static void update_tqs() {
+	FILE *f;
+	struct router *router;
+	char path[PATH_MAX];
+	char *line = NULL;
+	size_t len = 0;
+	uint8_t tq;
+	macaddr_t mac_a, mac_b;
+
+	// reset values
+	foreach(router, G.routers) {
+		router->tq = 0;
+		memset(router->originator, 0, sizeof(macaddr_t));
+	}
+
+	// TODO: Currently, we iterate over the whole list of routers all the
+	// time. Maybe it would be a good idea to sort routers that already
+	// have the current piece of information to the back. That way, we
+	// could abort as soon as we hit the first router with the current
+	// information filled in.
+
+	// translate all router's MAC addresses to originators simultaneously
+	snprintf(path, PATH_MAX, TRANSTABLE_GLOBAL, G.mesh_iface);
+	f = fopen(path, "r");
+	while (getline(&line, &len, f) != -1) {
+		if (sscanf(line, " * " F_MAC " (%*3u) via " F_MAC " (%*3u) (0x%*4x) [%*3c]",
+				F_MAC_VAR(&mac_a), F_MAC_VAR(&mac_b)) != 12)
+			continue;
+
+		foreach(router, G.routers) {
+			if (!memcmp(router->src, mac_a, sizeof(macaddr_t))) {
+				memcpy(router->originator, mac_b, sizeof(macaddr_t));
+				break; // foreach
+			}
+		}
+	}
+	fclose(f);
+
+	// look up TQs of originators
+	G.max_tq = 0;
+	snprintf(path, PATH_MAX, ORIGINATORS, G.mesh_iface);
+	f = fopen(path, "r");
+	while (getline(&line, &len, f) != -1) {
+		if (sscanf(line, F_MAC " %*fs (%hhu) " F_MAC_IGN "[ %*s]: " F_MAC_IGN " (%*3u)",
+				F_MAC_VAR(&mac_a), &tq) != 7)
+			continue;
+
+		foreach(router, G.routers) {
+			if (!memcmp(router->originator, mac_a, sizeof(macaddr_t))) {
+				router->tq = tq;
+				if (tq > G.max_tq)
+					G.max_tq = tq;
+				break; // foreach
+			}
+		}
+	}
+	fclose(f);
+
+	// 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) {
+		// rate local routers (if present) the highest
+		snprintf(path, PATH_MAX, TRANSTABLE_LOCAL, G.mesh_iface);
+		f = fopen(path, "r");
+		while (getline(&line, &len, f) != -1) {
+			if (sscanf(line, " * " F_MAC "[%*5s] %*f", F_MAC_VAR(&mac_a)) != 6)
+				continue;
+
+			foreach(router, G.routers) {
+				if (!memcmp(router->src, mac_a, sizeof(macaddr_t))) {
+					router->tq = LOCAL_TQ;
+					G.max_tq = LOCAL_TQ;
+					break; // foreach
+				}
+			}
+		}
+		fclose(f);
+	}
+
+	foreach(router, G.routers) {
+		if (router->tq == 0) {
+			fprintf(stderr, "didn't find TQ for non-local " F_MAC "\n", F_MAC_VAR(router->src));
+		}
+	}
+
+	free(line);
+}
+
+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;
+	sigemptyset(&signals);
+	sigaddset(&signals, SIGCHLD);
+
+	child = fork();
+	if (!child) {
+		// casting discards const, but should be safe
+		// (see http://stackoverflow.com/q/36925388)
+		execvp(file, (char**) argv);
+		error(1, errno, "can't execvp(\"%s\", ...)", file);
+	}
+
+	sigprocmask(SIG_BLOCK, &signals, NULL);
+	ret = sigtimedwait(&signals, &info, timeout);
+	sigprocmask(SIG_UNBLOCK, &signals, NULL);
+
+	if (ret == SIGCHLD) {
+		if (info.si_pid != child) {
+			cleanup();
+			error_at_line(1, 0, __FILE__, __LINE__,
+				"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(0, 0, "warning: child %d took too long, killing", child);
+	else if (ret < 0)
+		warn_errno("sigtimedwait failed, killing child");
+	else
+		error_at_line(1, 0, __FILE__, __LINE__,
+				"BUG: sigtimedwait() return some other signal than SIGCHLD: %d",
+				ret);
+
+	kill(child, SIGKILL);
+	kill(child, SIGCONT);
+	waitpid(child, NULL, 0);
+	return -1;
+}
+
+static void update_ebtables() {
+	struct timespec timeout = {
+		.tv_nsec = EBTABLES_TIMEOUT,
+	};
+	char mac[18];
+	struct router *router;
+
+	if (G.best_router && G.best_router->tq >= G.max_tq - G.hysteresis_thresh) {
+		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;
+		}
+	}
+	DEBUG_MSG("Determined %s as new best router with TQ=%d", mac, G.max_tq);
+	G.best_router = router;
+
+	if (fork_execvp_timeout(&timeout, "ebtables", (const char *[])
+			{ "ebtables", "-F", G.chain, NULL }))
+		error(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", "-A", G.chain, "-s", mac, "-j", "ACCEPT", NULL }))
+		error(0, 0, "warning: adding new rule to ebtables chain %s failed", G.chain);
+}
+
+int main(int argc, char *argv[]) {
+	int retval;
+	fd_set rfds;
+	struct timeval tv;
+	time_t last_update = time(NULL);
+
+	parse_cmdline(argc, argv);
+
+	if (G.sock == 0)
+		usage("No interface set!");
+
+	if (G.chain == NULL)
+		usage("No chain set!");
+
+	while (1) {
+		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)
+			exit_errno("select() failed");
+		else if (retval) {
+			if (FD_ISSET(G.sock, &rfds)) {
+				handle_ra(G.sock);
+			}
+		}
+		else
+			DEBUG_MSG("select() timeout expired");
+
+		if (G.routers != NULL && last_update <= time(NULL) - MIN_INTERVAL) {
+			expire_routers();
+
+			// all routers could have expired, check again
+			if (G.routers != NULL) {
+				update_tqs();
+				update_ebtables();
+				last_update = time(NULL);
+			}
+		}
+	}
+
+	cleanup();
+	return 0;
+}