diff --git a/package/gluon-radv-filterd/Makefile b/package/gluon-radv-filterd/Makefile new file mode 100644 index 00000000..fbad5b4d --- /dev/null +++ b/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)) diff --git a/package/gluon-radv-filterd/README.md b/package/gluon-radv-filterd/README.md new file mode 100644 index 00000000..01e104e0 --- /dev/null +++ b/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. diff --git a/package/gluon-radv-filterd/check_site.lua b/package/gluon-radv-filterd/check_site.lua new file mode 100644 index 00000000..242804bc --- /dev/null +++ b/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 diff --git a/package/gluon-radv-filterd/files/etc/config/gluon-radv-filterd b/package/gluon-radv-filterd/files/etc/config/gluon-radv-filterd new file mode 100644 index 00000000..32e1bbb9 --- /dev/null +++ b/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' 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 00000000..8fc08b1e --- /dev/null +++ b/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" +} 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 00000000..178084d4 --- /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/luasrc/lib/gluon/upgrade/300-gluon-radv-filterd b/package/gluon-radv-filterd/luasrc/lib/gluon/upgrade/300-gluon-radv-filterd new file mode 100644 index 00000000..e177d3f5 --- /dev/null +++ b/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 diff --git a/package/gluon-radv-filterd/src/Makefile b/package/gluon-radv-filterd/src/Makefile new file mode 100644 index 00000000..652c6703 --- /dev/null +++ b/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) 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 00000000..91b5dd4a --- /dev/null +++ b/package/gluon-radv-filterd/src/gluon-radv-filterd.c @@ -0,0 +1,513 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +// 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 ] [-t ] -c -i \n\n" + " -m B.A.T.M.A.N. advanced mesh interface used to get metric\n" + " information (\"TQ\") for the available gateways. Default: bat0\n" + " -t Minimum TQ difference required to switch the gateway.\n" + " Default: 0\n" + " -c 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 Interface to listen on for router advertisements. Should be\n" + " 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; +}