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}, + {} +};