From cf329daaf088fd609e6992ec73252ea6d0aded99 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Litza Date: Sat, 30 Jul 2016 00:19:06 +0200 Subject: [PATCH] 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. --- docs/index.rst | 1 + docs/package/gluon-radv-filterd.rst | 61 ++ package/gluon-radv-filterd/Makefile | 42 + package/gluon-radv-filterd/check_site.lua | 1 + .../files/etc/init.d/gluon-radv-filterd | 22 + .../files/lib/gluon/ebtables/400-radv-filterd | 3 + package/gluon-radv-filterd/src/Makefile | 49 ++ .../src/gluon-radv-filterd.c | 796 ++++++++++++++++++ package/gluon-radv-filterd/src/mac.h | 18 + package/gluon-radv-filterd/src/respondd.c | 49 ++ 10 files changed, 1042 insertions(+) create mode 100644 docs/package/gluon-radv-filterd.rst create mode 100644 package/gluon-radv-filterd/Makefile create mode 100644 package/gluon-radv-filterd/check_site.lua create mode 100755 package/gluon-radv-filterd/files/etc/init.d/gluon-radv-filterd create mode 100644 package/gluon-radv-filterd/files/lib/gluon/ebtables/400-radv-filterd create mode 100644 package/gluon-radv-filterd/src/Makefile create mode 100644 package/gluon-radv-filterd/src/gluon-radv-filterd.c create mode 100644 package/gluon-radv-filterd/src/mac.h create mode 100644 package/gluon-radv-filterd/src/respondd.c diff --git a/docs/index.rst b/docs/index.rst index be715325..5fe481c0 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 00000000..88949611 --- /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 00000000..4dd2ebba --- /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 00000000..fa38475d --- /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 00000000..04906e56 --- /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 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/src/Makefile b/package/gluon-radv-filterd/src/Makefile new file mode 100644 index 00000000..17b65584 --- /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 00000000..990885ea --- /dev/null +++ b/package/gluon-radv-filterd/src/gluon-radv-filterd.c @@ -0,0 +1,796 @@ +/* + Copyright (c) 2016 Jan-Philipp Litza + Copyright (c) 2017 Sven Eckelmann + 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#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 ] [-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_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 00000000..cc24d907 --- /dev/null +++ b/package/gluon-radv-filterd/src/mac.h @@ -0,0 +1,18 @@ +#include +#include + +#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 00000000..7e65a3d9 --- /dev/null +++ b/package/gluon-radv-filterd/src/respondd.c @@ -0,0 +1,49 @@ +#include + +#include +#include +#include +#include + +#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}, + {} +};