diff --git a/feeds/ucentral/spotfilter/Makefile b/feeds/ucentral/spotfilter/Makefile new file mode 100644 index 000000000..92e23ab2a --- /dev/null +++ b/feeds/ucentral/spotfilter/Makefile @@ -0,0 +1,57 @@ +# +# Copyright (C) 2021 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk +include $(INCLUDE_DIR)/kernel.mk + +PKG_NAME:=spotfilter +PKG_VERSION:=1 + +PKG_LICENSE:=GPL-2.0 +PKG_MAINTAINER:=Felix Fietkau + +PKG_BUILD_DEPENDS:=bpf-headers +PKG_FLAGS:=nonshared + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk +include $(INCLUDE_DIR)/bpf.mk +include $(INCLUDE_DIR)/nls.mk + +define Package/spotfilter + SECTION:=utils + CATEGORY:=Utilities + TITLE:=Network filter for hotspot services + DEPENDS:=+libbpf +libubox +libubus +libnl-tiny +kmod-sched-cake +kmod-sched-bpf $(BPF_DEPENDS) +endef + +TARGET_CFLAGS += \ + -Wno-error=deprecated-declarations \ + -I$(STAGING_DIR)/usr/include/libnl-tiny \ + -I$(STAGING_DIR)/usr/include -g3 + +CMAKE_OPTIONS += \ + -DLIBNL_LIBS=-lnl-tiny + +define Build/Compile + $(call CompileBPF,$(PKG_BUILD_DIR)/spotfilter-bpf.c) + $(Build/Compile/Default) +endef + +define Package/spotfilter/install + $(INSTALL_DIR) \ + $(1)/etc/hotplug.d/net \ + $(1)/etc/init.d \ + $(1)/lib/bpf \ + $(1)/usr/sbin + $(INSTALL_DATA) $(PKG_BUILD_DIR)/spotfilter-bpf.o $(1)/lib/bpf + $(INSTALL_BIN) ./files/spotfilter.init $(1)/etc/init.d/spotfilter + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/spotfilter $(1)/usr/sbin/ + $(INSTALL_DATA) ./files/spotfilter.hotplug $(1)/etc/hotplug.d/net/10-spotfilter +endef + +$(eval $(call BuildPackage,spotfilter)) diff --git a/feeds/ucentral/spotfilter/files/spotfilter.hotplug b/feeds/ucentral/spotfilter/files/spotfilter.hotplug new file mode 100644 index 000000000..a926d490d --- /dev/null +++ b/feeds/ucentral/spotfilter/files/spotfilter.hotplug @@ -0,0 +1,2 @@ +#!/bin/sh +ubus call spotfilter check_devices diff --git a/feeds/ucentral/spotfilter/files/spotfilter.init b/feeds/ucentral/spotfilter/files/spotfilter.init new file mode 100644 index 000000000..5c6d95fb7 --- /dev/null +++ b/feeds/ucentral/spotfilter/files/spotfilter.init @@ -0,0 +1,23 @@ +#!/bin/sh /etc/rc.common +# Copyright (c) 2021 OpenWrt.org + +START=18 + +USE_PROCD=1 +PROG=/usr/sbin/spotfilter + +reload_service() { + ubus call spotfilter interface_add "$(cat /tmp/spotfilter.json)" +} + +start_service() { + procd_open_instance + procd_set_param command "$PROG" + procd_set_param respawn + procd_close_instance +} + +service_started() { + ubus -t 10 wait_for spotfilter + [ $? = 0 ] && reload_service +} diff --git a/feeds/ucentral/spotfilter/src/CMakeLists.txt b/feeds/ucentral/spotfilter/src/CMakeLists.txt new file mode 100644 index 000000000..f9550e60c --- /dev/null +++ b/feeds/ucentral/spotfilter/src/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10) + +PROJECT(spotfilter C) + +ADD_DEFINITIONS(-Os -Wall -Wno-unknown-warning-option -Wno-array-bounds -Wno-format-truncation -Werror --std=gnu99) + +SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") + +IF (NOT DEFINED LIBNL_LIBS) + include(FindPkgConfig) + pkg_search_module(LIBNL libnl-3.0 libnl-3 libnl nl-3 nl) + IF (LIBNL_FOUND) + include_directories(${LIBNL_INCLUDE_DIRS}) + SET(LIBNL_LIBS ${LIBNL_LIBRARIES}) + ENDIF() +ENDIF() + +find_library(bpf NAMES bpf) +ADD_EXECUTABLE(spotfilter main.c bpf.c ubus.c rtnl.c interface.c snoop.c client.c dhcpv4.c icmpv6.c nl80211.c) +TARGET_LINK_LIBRARIES(spotfilter ${bpf} ubox ubus ${LIBNL_LIBS}) + +INSTALL(TARGETS spotfilter + RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR} +) diff --git a/feeds/ucentral/spotfilter/src/bpf.c b/feeds/ucentral/spotfilter/src/bpf.c new file mode 100644 index 000000000..83504b060 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/bpf.c @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include +#include + +#include "spotfilter.h" + +static int spotfilter_bpf_pr(enum libbpf_print_level level, const char *format, + va_list args) +{ + return vfprintf(stderr, format, args); +} + +static void +spotfilter_fill_rodata(struct bpf_object *obj, struct spotfilter_bpf_config *val) +{ + struct bpf_map *map = NULL; + + while ((map = bpf_object__next_map(obj, map)) != NULL) { + if (!strstr(bpf_map__name(map), ".rodata")) + continue; + + bpf_map__set_initial_value(map, val, sizeof(*val)); + } +} + +static void spotfilter_init_env(void) +{ + struct rlimit limit = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + setrlimit(RLIMIT_MEMLOCK, &limit); +} + +int spotfilter_bpf_load(struct interface *iface) +{ + DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts); + struct spotfilter_bpf_config config = { + .snoop_ifindex = spotfilter_ifb_ifindex + }; + struct bpf_program *prog_i, *prog_e; + struct bpf_object *obj; + int err; + + libbpf_set_print(spotfilter_bpf_pr); + + spotfilter_init_env(); + + obj = bpf_object__open_file(SPOTFILTER_PROG_PATH, &opts); + err = libbpf_get_error(obj); + if (err) { + perror("bpf_object__open_file"); + return -1; + } + + prog_i = bpf_object__find_program_by_name(obj, "spotfilter_in"); + if (!prog_i) { + fprintf(stderr, "Can't find ingress classifier\n"); + goto error; + } + + prog_e = bpf_object__find_program_by_name(obj, "spotfilter_out"); + if (!prog_e) { + fprintf(stderr, "Can't find egress classifier\n"); + goto error; + } + + bpf_program__set_type(prog_i, BPF_PROG_TYPE_SCHED_CLS); + bpf_program__set_type(prog_e, BPF_PROG_TYPE_SCHED_CLS); + + spotfilter_fill_rodata(obj, &config); + + err = bpf_object__load(obj); + if (err) { + perror("bpf_object__load"); + goto error; + } + + iface->bpf.prog_ingress = bpf_program__fd(prog_i); + iface->bpf.prog_egress = bpf_program__fd(prog_e); + if ((iface->bpf.map_class = bpf_object__find_map_fd_by_name(obj, "class")) < 0 || + (iface->bpf.map_client = bpf_object__find_map_fd_by_name(obj, "client")) < 0 || + (iface->bpf.map_whitelist_v4 = bpf_object__find_map_fd_by_name(obj, "whitelist_ipv4")) < 0 || + (iface->bpf.map_whitelist_v6 = bpf_object__find_map_fd_by_name(obj, "whitelist_ipv6")) < 0) { + perror("bpf_object__find_map_fd_by_name"); + goto error; + } + iface->bpf.obj = obj; + + return 0; + +error: + bpf_object__close(obj); + return -1; +} + +int spotfilter_bpf_get_client(struct interface *iface, + const struct spotfilter_client_key *key, + struct spotfilter_client_data *data) +{ + return bpf_map_lookup_elem(iface->bpf.map_client, key, data); +} + +int spotfilter_bpf_set_client(struct interface *iface, + const struct spotfilter_client_key *key, + const struct spotfilter_client_data *data) +{ + if (!data) + return bpf_map_delete_elem(iface->bpf.map_client, key); + + return bpf_map_update_elem(iface->bpf.map_client, key, data, BPF_ANY); +} + +static void +__spotfilter_bpf_set_device(struct interface *iface, int ifindex, bool egress, bool enabled) +{ + DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, + .attach_point = egress ? BPF_TC_EGRESS : BPF_TC_INGRESS, + .ifindex = ifindex); + DECLARE_LIBBPF_OPTS(bpf_tc_opts, attach_tc, + .handle = 1, + .priority = SPOTFILTER_PRIO_BASE); + + if (!enabled) { + bpf_tc_detach(&hook, &attach_tc); + return; + } + + if (egress) + attach_tc.prog_fd = iface->bpf.prog_egress; + else + attach_tc.prog_fd = iface->bpf.prog_ingress; + + bpf_tc_hook_create(&hook); + bpf_tc_attach(&hook, &attach_tc); +} + +void spotfilter_bpf_set_device(struct interface *iface, int ifindex, bool enabled) +{ + if (enabled) + spotfilter_bpf_set_device(iface, ifindex, false); + + __spotfilter_bpf_set_device(iface, ifindex, true, enabled); + __spotfilter_bpf_set_device(iface, ifindex, false, enabled); +} + +void spotfilter_bpf_update_class(struct interface *iface, uint32_t index) +{ + bpf_map_update_elem(iface->bpf.map_class, &index, &iface->cdata[index], BPF_ANY); +} + +bool spotfilter_bpf_whitelist_seen(struct interface *iface, const void *addr, bool ipv6) +{ + int fd = ipv6 ? iface->bpf.map_whitelist_v6 : iface->bpf.map_whitelist_v4; + struct spotfilter_whitelist_entry e; + + bpf_map_lookup_elem(fd, addr, &e); + if (!e.seen) + return false; + + e.seen = 0; + bpf_map_update_elem(fd, addr, &e, BPF_ANY); + + return true; +} + +void spotfilter_bpf_set_whitelist(struct interface *iface, const void *addr, + bool ipv6, const uint8_t *state) +{ + int fd = ipv6 ? iface->bpf.map_whitelist_v6 : iface->bpf.map_whitelist_v4; + struct spotfilter_whitelist_entry e = {}; + + if (!state) { + bpf_map_delete_elem(fd, addr); + return; + } + + e.val = *state; + bpf_map_update_elem(fd, addr, &e, BPF_ANY); +} + +void spotfilter_bpf_free(struct interface *iface) +{ + if (!iface->bpf.obj) + return; + + bpf_object__close(iface->bpf.obj); + iface->bpf.obj = NULL; +} diff --git a/feeds/ucentral/spotfilter/src/bpf.h b/feeds/ucentral/spotfilter/src/bpf.h new file mode 100644 index 000000000..747572a52 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/bpf.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __SPOTFILTER_BPF_H +#define __SPOTFILTER_BPF_H + +struct interface; + +int spotfilter_bpf_load(struct interface *iface); +void spotfilter_bpf_free(struct interface *iface); +void spotfilter_bpf_set_device(struct interface *iface, int ifindex, bool enabled); +void spotfilter_bpf_update_class(struct interface *iface, uint32_t index); +int spotfilter_bpf_get_client(struct interface *iface, + const struct spotfilter_client_key *key, + struct spotfilter_client_data *data); +int spotfilter_bpf_set_client(struct interface *iface, + const struct spotfilter_client_key *key, + const struct spotfilter_client_data *data); +void spotfilter_bpf_set_whitelist(struct interface *iface, const void *addr, + bool ipv6, const uint8_t *state); +bool spotfilter_bpf_whitelist_seen(struct interface *iface, const void *addr, bool ipv6); + +#endif diff --git a/feeds/ucentral/spotfilter/src/bpf_skb_utils.h b/feeds/ucentral/spotfilter/src/bpf_skb_utils.h new file mode 100644 index 000000000..e6d5f00c7 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/bpf_skb_utils.h @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __BPF_SKB_UTILS_H +#define __BPF_SKB_UTILS_H + +#include +#include +#include +#include +#include +#include +#include +#include + +struct skb_parser_info { + struct __sk_buff *skb; + __u32 offset; + int proto; +}; + +static __always_inline void *__skb_data(struct __sk_buff *skb) +{ + return (void *)(long)READ_ONCE(skb->data); +} + +static __always_inline void * +skb_ptr(struct __sk_buff *skb, __u32 offset, __u32 len) +{ + void *ptr = __skb_data(skb) + offset; + void *end = (void *)(long)(skb->data_end); + + if (ptr + len >= end) + return NULL; + + return ptr; +} + +static __always_inline void * +skb_info_ptr(struct skb_parser_info *info, __u32 len) +{ + __u32 offset = info->offset; + return skb_ptr(info->skb, offset, len); +} + +static __always_inline void +skb_parse_init(struct skb_parser_info *info, struct __sk_buff *skb) +{ + *info = (struct skb_parser_info){ + .skb = skb + }; +} + +static __always_inline struct ethhdr * +skb_parse_ethernet(struct skb_parser_info *info) +{ + struct ethhdr *eth; + int len; + + len = sizeof(*eth) + 2 * sizeof(struct vlan_hdr) + sizeof(struct ipv6hdr); + if (len > info->skb->len) + len = info->skb->len; + bpf_skb_pull_data(info->skb, len); + + eth = skb_info_ptr(info, sizeof(*eth)); + if (!eth) + return NULL; + + info->proto = eth->h_proto; + info->offset += sizeof(*eth); + + return eth; +} + +static __always_inline struct vlan_hdr * +skb_parse_vlan(struct skb_parser_info *info) +{ + struct vlan_hdr *vlh; + + if (info->proto != bpf_htons(ETH_P_8021Q) && + info->proto != bpf_htons(ETH_P_8021AD)) + return NULL; + + vlh = skb_info_ptr(info, sizeof(*vlh)); + if (!vlh) + return NULL; + + info->proto = vlh->h_vlan_encapsulated_proto; + info->offset += sizeof(*vlh); + + return vlh; +} + +static __always_inline struct iphdr * +skb_parse_ipv4(struct skb_parser_info *info, int min_l4_bytes) +{ + struct iphdr *iph; + int proto, hdr_len; + __u32 pull_len; + + if (info->proto != bpf_htons(ETH_P_IP)) + return NULL; + + iph = skb_info_ptr(info, sizeof(*iph)); + if (!iph) + return NULL; + + hdr_len = iph->ihl * 4; + if (hdr_len < sizeof(*iph)) + return NULL; + + pull_len = info->offset + hdr_len + min_l4_bytes; + if (pull_len > info->skb->len) + pull_len = info->skb->len; + + if (bpf_skb_pull_data(info->skb, pull_len)) + return NULL; + + iph = skb_info_ptr(info, sizeof(*iph)); + if (!iph) + return NULL; + + info->proto = iph->protocol; + info->offset += hdr_len; + + return iph; +} + +static __always_inline struct ipv6hdr * +skb_parse_ipv6(struct skb_parser_info *info, int max_l4_bytes) +{ + struct ipv6hdr *ip6h; + __u32 pull_len; + + if (info->proto != bpf_htons(ETH_P_IPV6)) + return NULL; + + pull_len = info->offset + sizeof(*ip6h) + max_l4_bytes; + if (pull_len > info->skb->len) + pull_len = info->skb->len; + + if (bpf_skb_pull_data(info->skb, pull_len)) + return NULL; + + ip6h = skb_info_ptr(info, sizeof(*ip6h)); + if (!ip6h) + return NULL; + + info->proto = READ_ONCE(ip6h->nexthdr); + info->offset += sizeof(*ip6h); + + return ip6h; +} + +static __always_inline struct tcphdr * +skb_parse_tcp(struct skb_parser_info *info) +{ + struct tcphdr *tcph; + + if (info->proto != IPPROTO_TCP) + return NULL; + + tcph = skb_info_ptr(info, sizeof(*tcph)); + if (!tcph) + return NULL; + + info->offset += tcph->doff * 4; + + return tcph; +} + +#endif diff --git a/feeds/ucentral/spotfilter/src/client.c b/feeds/ucentral/spotfilter/src/client.c new file mode 100644 index 000000000..005488e88 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/client.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include "spotfilter.h" + +#define CACHE_TIMEOUT 10 + +struct cache_entry { + struct avl_node node; + uint8_t macaddr[ETH_ALEN]; + uint32_t ip4addr; + uint32_t ip6addr[4]; + uint32_t time; +}; + +static int avl_mac_cmp(const void *k1, const void *k2, void *priv) +{ + return memcmp(k1, k2, ETH_ALEN); +} + +static AVL_TREE(cache, avl_mac_cmp, false, NULL); + +static uint32_t client_gettime(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return ts.tv_sec; +} + +static void client_gc(struct uloop_timeout *t) +{ + struct cache_entry *c, *tmp; + uint32_t now = client_gettime(); + + avl_for_each_element_safe(&cache, c, node, tmp) { + uint32_t diff; + + diff = now - c->time; + if (diff < CACHE_TIMEOUT) + continue; + + avl_delete(&cache, &c->node); + free(c); + } + + if (!avl_is_empty(&cache)) + uloop_timeout_set(t, 1000); +} + +void client_init_interface(struct interface *iface) +{ + avl_init(&iface->clients, avl_mac_cmp, false, NULL); + avl_init(&iface->client_ids, avl_strcmp, false, NULL); +} + +static void __client_free(struct interface *iface, struct client *cl) +{ + if (cl->id_node.key) + avl_delete(&iface->client_ids, &cl->id_node); + avl_delete(&iface->clients, &cl->node); + kvlist_free(&cl->kvdata); + spotfilter_bpf_set_client(iface, &cl->key, NULL); + free(cl); +} + +void client_free(struct interface *iface, struct client *cl) +{ + spotfilter_ubus_notify(iface, cl, "client_delete"); + __client_free(iface, cl); +} + +static void client_set_id(struct interface *iface, struct client *cl, const char *id) +{ + if (id == cl->id_node.key) + return; + + if (id && cl->id_node.key && !strcmp(id, cl->id_node.key)) + return; + + if (cl->id_node.key) { + avl_delete(&iface->client_ids, &cl->id_node); + free((void *) cl->id_node.key); + cl->id_node.key = NULL; + } + + if (!id) + return; + + cl->id_node.key = strdup(id); + avl_insert(&iface->client_ids, &cl->id_node); +} + +int client_set(struct interface *iface, const void *addr, const char *id, + int state, int dns_state, int accounting, struct blob_attr *data) +{ + struct cache_entry *c; + struct blob_attr *cur; + struct client *cl; + bool new_client = false; + int rem; + + cl = avl_find_element(&iface->clients, addr, cl, node); + if (!cl) { + cl = calloc(1, sizeof(*cl)); + cl->node.key = &cl->key.addr; + memcpy(cl->key.addr, addr, ETH_ALEN); + avl_insert(&iface->clients, &cl->node); + cl->data.cur_class = iface->default_class; + cl->data.dns_class = iface->default_dns_class; + kvlist_init(&cl->kvdata, kvlist_blob_len); + new_client = true; + } + + client_set_id(iface, cl, id); + if (!new_client) + spotfilter_bpf_get_client(iface, &cl->key, &cl->data); + + c = avl_find_element(&cache, addr, c, node); + if (c) { + if (!cl->data.ip4addr) + cl->data.ip4addr = c->ip4addr; + if (!cl->data.ip6addr[0]) + memcpy(cl->data.ip6addr, c->ip6addr, sizeof(cl->data.ip6addr)); + } + + if (state >= SPOTFILTER_NUM_CLASS || dns_state >= SPOTFILTER_NUM_CLASS) { + if (new_client) + __client_free(iface, cl); + + return -1; + } + + blobmsg_for_each_attr(cur, data, rem) { + if (!blobmsg_check_attr(cur, true)) + continue; + + kvlist_set(&cl->kvdata, blobmsg_name(cur), cur); + } + if (state >= 0) + cl->data.cur_class = state; + if (dns_state >= 0) + cl->data.dns_class = dns_state; + if (accounting >= 0) + cl->data.flags = accounting; + spotfilter_bpf_set_client(iface, &cl->key, &cl->data); + + if (new_client) + spotfilter_ubus_notify(iface, cl, "client_add"); + + return 0; +} + +void client_set_ipaddr(const void *mac, const void *addr, bool ipv6) +{ + static struct uloop_timeout gc_timer = { + .cb = client_gc + }; + struct interface *iface; + struct cache_entry *c; + struct client *cl; + + c = avl_find_element(&cache, mac, c, node); + if (!c) { + c = calloc(1, sizeof(*c)); + memcpy(c->macaddr, mac, ETH_ALEN); + c->node.key = c->macaddr; + avl_insert(&cache, &c->node); + if (!gc_timer.pending) + uloop_timeout_set(&gc_timer, CACHE_TIMEOUT * 1000); + } + + if (!ipv6 && !c->ip4addr) + memcpy(&c->ip4addr, addr, sizeof(c->ip4addr)); + else if (ipv6 && !c->ip6addr[0]) + memcpy(&c->ip6addr, addr, sizeof(c->ip6addr)); + else + return; + + c->time = client_gettime(); + + avl_for_each_element(&interfaces, iface, node) { + cl = avl_find_element(&iface->clients, mac, cl, node); + if (!cl) + continue; + + spotfilter_bpf_get_client(iface, &cl->key, &cl->data); + + if (!ipv6 && !cl->data.ip4addr) + memcpy(&cl->data.ip4addr, addr, sizeof(cl->data.ip4addr)); + else if (ipv6 && !cl->data.ip6addr[0]) + memcpy(&cl->data.ip6addr, addr, sizeof(cl->data.ip6addr)); + else + continue; + + spotfilter_bpf_set_client(iface, &cl->key, &cl->data); + } +} diff --git a/feeds/ucentral/spotfilter/src/client.h b/feeds/ucentral/spotfilter/src/client.h new file mode 100644 index 000000000..722ead082 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/client.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __SPOTFILTER_CLIENT_H +#define __SPOTFILTER_CLIENT_H + +#include +#include + +struct client { + struct avl_node node; + struct avl_node id_node; + + struct kvlist kvdata; + int idle; + + struct spotfilter_client_key key; + struct spotfilter_client_data data; +}; + +int client_set(struct interface *iface, const void *addr, const char *id, + int state, int dns_state, int accounting, struct blob_attr *data); +void client_free(struct interface *iface, struct client *cl); +void client_set_ipaddr(const void *mac, const void *addr, bool ipv6); +void client_init_interface(struct interface *iface); + +#endif diff --git a/feeds/ucentral/spotfilter/src/dhcpv4.c b/feeds/ucentral/spotfilter/src/dhcpv4.c new file mode 100644 index 000000000..661ff9745 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/dhcpv4.c @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include "spotfilter.h" + +enum dhcpv4_msg { + DHCPV4_MSG_DISCOVER = 1, + DHCPV4_MSG_OFFER = 2, + DHCPV4_MSG_REQUEST = 3, + DHCPV4_MSG_DECLINE = 4, + DHCPV4_MSG_ACK = 5, + DHCPV4_MSG_NAK = 6, + DHCPV4_MSG_RELEASE = 7, + DHCPV4_MSG_INFORM = 8, + DHCPV4_MSG_FORCERENEW = 9, +}; + +enum dhcpv4_opt { + DHCPV4_OPT_PAD = 0, + DHCPV4_OPT_NETMASK = 1, + DHCPV4_OPT_ROUTER = 3, + DHCPV4_OPT_DNSSERVER = 6, + DHCPV4_OPT_DOMAIN = 15, + DHCPV4_OPT_MTU = 26, + DHCPV4_OPT_BROADCAST = 28, + DHCPV4_OPT_NTPSERVER = 42, + DHCPV4_OPT_LEASETIME = 51, + DHCPV4_OPT_MESSAGE = 53, + DHCPV4_OPT_SERVERID = 54, + DHCPV4_OPT_REQOPTS = 55, + DHCPV4_OPT_RENEW = 58, + DHCPV4_OPT_REBIND = 59, + DHCPV4_OPT_IPADDRESS = 50, + DHCPV4_OPT_MSG_TYPE = 53, + DHCPV4_OPT_HOSTNAME = 12, + DHCPV4_OPT_REQUEST = 17, + DHCPV4_OPT_USER_CLASS = 77, + DHCPV4_OPT_AUTHENTICATION = 90, + DHCPV4_OPT_SEARCH_DOMAIN = 119, + DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE = 145, + DHCPV4_OPT_END = 255, +}; + +struct dhcpv4_message { + uint8_t op; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + uint32_t xid; + uint16_t secs; + uint16_t flags; + struct in_addr ciaddr; + struct in_addr yiaddr; + struct in_addr siaddr; + struct in_addr giaddr; + uint8_t chaddr[16]; + char sname[64]; + char file[128]; + uint32_t magic; + uint8_t options[]; +} __attribute__((packed)); + +#define DHCPV4_MAGIC 0x63825363 + +struct dhcpv4_option { + uint8_t type; + uint8_t len; + uint8_t data[]; +}; + +#define dhcpv4_for_each_option(opt, start, end) \ + for (opt = (const struct dhcpv4_option *)(start); \ + &opt[1] <= (const struct dhcpv4_option *)(end) && \ + &opt->data[opt->len] <= (const uint8_t *)(end); \ + opt = (const struct dhcpv4_option *)&opt->data[opt->len]) + +void spotfilter_recv_dhcpv4(const void *msgdata, int len, const void *eth_addr) +{ + const struct dhcpv4_message *msg = msgdata; + const struct dhcpv4_option *opt; + uint8_t bcast_addr[ETH_ALEN] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + int op = -1; + + if (ntohl(msg->magic) != DHCPV4_MAGIC) + return; + + if (msg->op != 2 || msg->htype != 1 || msg->hlen != 6) + return; + + if (memcmp(eth_addr, bcast_addr, ETH_ALEN) != 0 && + memcmp(eth_addr, msg->chaddr, ETH_ALEN) != 0) + return; + + dhcpv4_for_each_option(opt, msg->options, msgdata + len) { + switch (opt->type) { + case DHCPV4_OPT_MESSAGE: + if (opt->len != 1) + break; + + op = opt->data[0]; + break; + } + } + + if (op != DHCPV4_MSG_ACK) + return; + + client_set_ipaddr(msg->chaddr, (uint32_t *)&msg->yiaddr, false); +} + diff --git a/feeds/ucentral/spotfilter/src/example.json b/feeds/ucentral/spotfilter/src/example.json new file mode 100644 index 000000000..7452d6c2d --- /dev/null +++ b/feeds/ucentral/spotfilter/src/example.json @@ -0,0 +1,36 @@ +{ + "name": "hotspot", + "devices": [ "wlan1" ], + "config": { + "class": [ + { + "index": 0 + }, + { + "index": 1, + "device_macaddr": "br-test" + }, + { + "index": 2, + "macaddr": "00:11:22:33:44:55" + }, + { + "index": 3, + "fwmark": 1, + "fwmark_mask": 127 + }, + { + "index": 4, + "redirect": "up0v2" + } + ], + "default_class": 1, + "default_dns_class": 0, + "whitelist": [ + { + "class": 0, + "hosts": [ "*.google.de", "*.google.com" ] + } + ] + } +} diff --git a/feeds/ucentral/spotfilter/src/icmpv6.c b/feeds/ucentral/spotfilter/src/icmpv6.c new file mode 100644 index 000000000..922d3d931 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/icmpv6.c @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include "spotfilter.h" + +struct icmpv6_opt { + uint8_t type; + uint8_t len; + uint8_t data[6]; +}; + +#define icmpv6_for_each_option(opt, start, end) \ + for (opt = (const struct icmpv6_opt*)(start); \ + (const void *)(opt + 1) <= (const void *)(end) && opt->len > 0 && \ + (const void *)(opt + opt->len) <= (const void *)(end); opt += opt->len) + +void spotfilter_recv_icmpv6(const void *data, int len, const uint8_t *src, const uint8_t *dest) +{ + const struct nd_neighbor_advert *nd = data; + const struct icmp6_hdr *hdr = data; + const struct icmpv6_opt *opt; + + if (len < sizeof(*nd) || hdr->icmp6_code) + return; + + if (hdr->icmp6_type != ND_NEIGHBOR_ADVERT) + return; + + icmpv6_for_each_option(opt, &nd[1], data + len) { + if (opt->type != ND_OPT_TARGET_LINKADDR || opt->len != 1) + continue; + + if (memcmp(opt->data, src, ETH_ALEN)) + return; + } + + if ((nd->nd_na_target.s6_addr[0] & 0xe0) != 0x20) + return; + + if (opt != (const struct icmpv6_opt *)(data + len)) + return; + + client_set_ipaddr(src, &nd->nd_na_target, true); +} diff --git a/feeds/ucentral/spotfilter/src/interface.c b/feeds/ucentral/spotfilter/src/interface.c new file mode 100644 index 000000000..fed457131 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/interface.c @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include +#include + +#include + +#include "spotfilter.h" + +AVL_TREE(interfaces, avl_strcmp, false, NULL); + +void interface_free(struct interface *iface) +{ + struct client *cl, *tmp; + + spotfilter_dns_free(iface); + + vlist_flush_all(&iface->devices); + + avl_for_each_element_safe(&iface->clients, cl, node, tmp) + client_free(iface, cl); + + spotfilter_bpf_free(iface); + + avl_delete(&interfaces, &iface->node); + free(iface->config); + free(iface); +} + +static inline const char * +device_name(struct device *dev) +{ + return dev->node.avl.key; +} + +static void +interface_check_device(struct interface *iface, struct device *dev) +{ + int old_ifindex = dev->ifindex; + + dev->ifindex = if_nametoindex(device_name(dev)); + if (dev->ifindex != old_ifindex) + spotfilter_bpf_set_device(iface, dev->ifindex, true); +} + +static void +device_update_cb(struct vlist_tree *tree, + struct vlist_node *node_new, + struct vlist_node *node_old) +{ + struct interface *iface = container_of(tree, struct interface, devices); + struct device *dev_new = container_of_safe(node_new, struct device, node); + struct device *dev_old = container_of_safe(node_old, struct device, node); + + if (dev_new) { + if (dev_old) + dev_new->ifindex = dev_old->ifindex; + interface_check_device(iface, dev_new); + } + + if (dev_old) { + if (!dev_new && dev_old->ifindex) + spotfilter_bpf_set_device(iface, dev_old->ifindex, false); + free(dev_old); + } +} + +static int +interface_parse_class(struct spotfilter_bpf_class *cdata, struct blob_attr *attr) +{ + enum { + CLASS_ATTR_INDEX, + CLASS_ATTR_DEV_MAC, + CLASS_ATTR_MAC, + CLASS_ATTR_REDIRECT, + CLASS_ATTR_FWMARK, + CLASS_ATTR_FWMARK_MASK, + __CLASS_ATTR_MAX, + }; + static const struct blobmsg_policy policy[__CLASS_ATTR_MAX] = { + [CLASS_ATTR_INDEX] = { "index", BLOBMSG_TYPE_INT32 }, + [CLASS_ATTR_DEV_MAC] = { "device_macaddr", BLOBMSG_TYPE_STRING }, + [CLASS_ATTR_MAC] = { "macaddr", BLOBMSG_TYPE_STRING }, + [CLASS_ATTR_REDIRECT] = { "redirect", BLOBMSG_TYPE_STRING }, + [CLASS_ATTR_FWMARK] = { "fwmark", BLOBMSG_TYPE_INT32 }, + [CLASS_ATTR_FWMARK_MASK] = { "fwmark_mask", BLOBMSG_TYPE_INT32 }, + }; + struct blob_attr *tb[__CLASS_ATTR_MAX]; + struct blob_attr *cur; + unsigned int index; + + if (blobmsg_type(attr) != BLOBMSG_TYPE_TABLE) + return -1; + + blobmsg_parse(policy, __CLASS_ATTR_MAX, tb, + blobmsg_data(attr), blobmsg_len(attr)); + + if ((cur = tb[CLASS_ATTR_INDEX]) != NULL) + index = blobmsg_get_u32(cur); + else + return -1; + + if (index >= SPOTFILTER_NUM_CLASS) + return -1; + + if ((cur = tb[CLASS_ATTR_MAC]) != NULL) { + void *addr; + + addr = ether_aton(blobmsg_get_string(cur)); + if (!addr) + goto invalid; + + memcpy(cdata->dest_mac, addr, sizeof(cdata->dest_mac)); + cdata->actions |= SPOTFILTER_ACTION_SET_DEST_MAC; + } else if ((cur = tb[CLASS_ATTR_DEV_MAC]) != NULL) { + const char *name = blobmsg_get_string(cur); + struct ifreq ifr = {}; + int sock; + int ret; + + if (strlen(name) > IFNAMSIZ) + goto invalid; + + strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); + + sock = socket(AF_INET, SOCK_DGRAM, 0); + ret = ioctl(sock, SIOCGIFHWADDR, &ifr); + if (ret < 0) + perror("ioctl"); + close(sock); + + if (ret < 0) + goto invalid; + + if (ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER) + goto invalid; + + memcpy(cdata->dest_mac, ifr.ifr_hwaddr.sa_data, sizeof(cdata->dest_mac)); + cdata->actions |= SPOTFILTER_ACTION_SET_DEST_MAC; + } + + if ((cur = tb[CLASS_ATTR_REDIRECT]) != NULL) { + unsigned int ifindex = if_nametoindex(blobmsg_get_string(cur)); + + if (!ifindex) + goto invalid; + + cdata->redirect_ifindex = ifindex; + cdata->actions |= SPOTFILTER_ACTION_REDIRECT; + } + + if ((cur = tb[CLASS_ATTR_FWMARK_MASK]) != NULL) + cdata->fwmark_mask = blobmsg_get_u32(cur); + else + cdata->fwmark_mask = ~0; + + if ((cur = tb[CLASS_ATTR_FWMARK]) != NULL) { + cdata->fwmark_val = blobmsg_get_u32(cur); + cdata->actions |= SPOTFILTER_ACTION_FWMARK; + } + + cdata->actions |= SPOTFILTER_ACTION_VALID; + return index; + +invalid: + cdata->actions = 0; + return index; +} + +static bool +__interface_check_whitelist(struct blob_attr *attr) +{ + enum { + WL_ATTR_CLASS, + WL_ATTR_HOSTS, + __WL_ATTR_MAX + }; + static const struct blobmsg_policy policy[__WL_ATTR_MAX] = { + [WL_ATTR_CLASS] = { "class", BLOBMSG_TYPE_INT32 }, + [WL_ATTR_HOSTS] = { "hosts", BLOBMSG_TYPE_ARRAY }, + }; + struct blob_attr *tb[__WL_ATTR_MAX]; + + blobmsg_parse(policy, __WL_ATTR_MAX, tb, blobmsg_data(attr), blobmsg_len(attr)); + + if (!tb[WL_ATTR_CLASS] || !tb[WL_ATTR_HOSTS]) + return false; + + return blobmsg_check_array(tb[WL_ATTR_HOSTS], BLOBMSG_TYPE_STRING) >= 0; +} + +static bool +interface_check_whitelist(struct blob_attr *attr) +{ + struct blob_attr *cur; + int rem; + + if (blobmsg_check_array(attr, BLOBMSG_TYPE_TABLE) <= 0) + return false; + + blobmsg_for_each_attr(cur, attr, rem) { + if (!__interface_check_whitelist(cur)) + return false; + } + + return true; +} + +static void +interface_set_config(struct interface *iface, bool iface_init) +{ + enum { + CONFIG_ATTR_CLASS, + CONFIG_ATTR_WHITELIST, + CONFIG_ATTR_ACTIVE_TIMEOUT, + CONFIG_ATTR_CLIENT_AUTOCREATE, + CONFIG_ATTR_CLIENT_AUTOREMOVE, + CONFIG_ATTR_CLIENT_TIMEOUT, + CONFIG_ATTR_DEFAULT_CLASS, + CONFIG_ATTR_DEFAULT_DNS_CLASS, + __CONFIG_ATTR_MAX, + }; + static const struct blobmsg_policy policy[__CONFIG_ATTR_MAX] = { + [CONFIG_ATTR_CLASS] = { "class", BLOBMSG_TYPE_ARRAY }, + [CONFIG_ATTR_WHITELIST] = { "whitelist", BLOBMSG_TYPE_ARRAY }, + [CONFIG_ATTR_ACTIVE_TIMEOUT] = { "active_timeout", BLOBMSG_TYPE_INT32 }, + [CONFIG_ATTR_CLIENT_TIMEOUT] = { "client_timeout", BLOBMSG_TYPE_INT32 }, + [CONFIG_ATTR_CLIENT_AUTOCREATE] = { "client_autocreate", BLOBMSG_TYPE_BOOL }, + [CONFIG_ATTR_CLIENT_AUTOREMOVE] = { "client_autoremove", BLOBMSG_TYPE_BOOL }, + [CONFIG_ATTR_DEFAULT_CLASS] = { "default_class", BLOBMSG_TYPE_INT32 }, + [CONFIG_ATTR_DEFAULT_DNS_CLASS] = { "default_dns_class", BLOBMSG_TYPE_INT32 }, + }; + struct blob_attr *tb[__CONFIG_ATTR_MAX]; + struct blob_attr *cur; + uint32_t class_mask = 0; + int i, rem; + + blobmsg_parse(policy, __CONFIG_ATTR_MAX, tb, + blobmsg_data(iface->config), blobmsg_len(iface->config)); + + if ((cur = tb[CONFIG_ATTR_DEFAULT_CLASS]) != NULL && + blobmsg_get_u32(cur) < SPOTFILTER_NUM_CLASS) + iface->default_class = blobmsg_get_u32(cur); + else + iface->default_class = 0; + + if ((cur = tb[CONFIG_ATTR_DEFAULT_DNS_CLASS]) != NULL && + blobmsg_get_u32(cur) < SPOTFILTER_NUM_CLASS) + iface->default_dns_class = blobmsg_get_u32(cur); + else + iface->default_dns_class = 0; + + if ((cur = tb[CONFIG_ATTR_WHITELIST]) != NULL && interface_check_whitelist(cur)) + iface->whitelist = cur; + else + iface->whitelist = NULL; + + if ((cur = tb[CONFIG_ATTR_ACTIVE_TIMEOUT]) != NULL) + iface->active_timeout = blobmsg_get_u32(cur); + else + iface->active_timeout = 300; + + if ((cur = tb[CONFIG_ATTR_CLIENT_TIMEOUT]) != NULL) + iface->client_timeout = blobmsg_get_u32(cur); + else + iface->client_timeout = 30; + + if ((cur = tb[CONFIG_ATTR_CLIENT_AUTOCREATE]) != NULL) + iface->client_autocreate = blobmsg_get_u8(cur); + else + iface->client_autocreate = true; + + if ((cur = tb[CONFIG_ATTR_CLIENT_AUTOREMOVE]) != NULL) + iface->client_autoremove = blobmsg_get_u8(cur); + else + iface->client_autoremove = true; + + blobmsg_for_each_attr(cur, tb[CONFIG_ATTR_CLASS], rem) { + struct spotfilter_bpf_class cdata = {}; + int index; + + index = interface_parse_class(&cdata, cur); + if (index < 0) + continue; + + if (iface_init || + memcmp(&iface->cdata[index], &cdata, sizeof(cdata)) != 0) { + memcpy(&iface->cdata[index], &cdata, sizeof(cdata)); + spotfilter_bpf_update_class(iface, index); + } + + class_mask |= 1 << index; + } + + for (i = 0; i < SPOTFILTER_NUM_CLASS; i++) { + if (class_mask & (1 << i)) + continue; + + memset(&iface->cdata[i], 0, sizeof(iface->cdata[i])); + spotfilter_bpf_update_class(iface, i); + } +} + +void interface_check_devices(void) +{ + struct interface *iface; + struct device *dev; + + avl_for_each_element(&interfaces, iface, node) { + interface_set_config(iface, false); + + vlist_for_each_element(&iface->devices, dev, node) + interface_check_device(iface, dev); + } +} + +void interface_add(const char *name, struct blob_attr *config, + struct blob_attr *devices) +{ + struct interface *iface; + struct blob_attr *cur; + char *name_buf; + bool iface_init = false; + int rem; + + iface = avl_find_element(&interfaces, name, iface, node); + if (!iface) { + iface = calloc_a(sizeof(*iface), &name_buf, strlen(name) + 1); + iface->node.key = strcpy(name_buf, name); + vlist_init(&iface->devices, avl_strcmp, device_update_cb); + client_init_interface(iface); + spotfilter_dns_init(iface); + + if (spotfilter_bpf_load(iface)) { + free(iface); + return; + } + + avl_insert(&interfaces, &iface->node); + iface_init = true; + } + + if (config && !blob_attr_equal(iface->config, config)) { + free(iface->config); + iface->config = blob_memdup(config); + interface_set_config(iface, iface_init); + } + + blobmsg_for_each_attr(cur, devices, rem) { + struct device *dev; + const char *name = blobmsg_get_string(cur); + + dev = calloc_a(sizeof(*dev), &name_buf, strlen(name) + 1); + vlist_add(&iface->devices, &dev->node, strcpy(name_buf, name)); + } +} + +void interface_done(void) +{ + struct interface *iface, *tmp; + + avl_for_each_element_safe(&interfaces, iface, node, tmp) + interface_free(iface); +} diff --git a/feeds/ucentral/spotfilter/src/interface.h b/feeds/ucentral/spotfilter/src/interface.h new file mode 100644 index 000000000..5f3b6cfa5 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/interface.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __SPOTFILTER_INTERFACE_H +#define __SPOTFILTER_INTERFACE_H + +#include +#include +#include + +struct bpf_object; + +struct interface { + struct avl_node node; + + struct blob_attr *config; + struct blob_attr *whitelist; + + struct avl_tree cname_cache; + struct avl_tree addr_map; + + struct uloop_timeout addr_gc; + uint32_t next_gc; + + uint32_t active_timeout; + + uint8_t default_class; + uint8_t default_dns_class; + + bool client_autocreate; + bool client_autoremove; + int client_timeout; + + struct { + struct bpf_object *obj; + + int prog_ingress; + int prog_egress; + int map_class; + int map_client; + int map_whitelist_v4; + int map_whitelist_v6; + } bpf; + + struct spotfilter_bpf_class cdata[SPOTFILTER_NUM_CLASS]; + + struct vlist_tree devices; + + struct avl_tree clients; + struct avl_tree client_ids; +}; + +struct device { + struct vlist_node node; + + int ifindex; +}; + +extern struct avl_tree interfaces; + +static inline const char *interface_name(struct interface *iface) +{ + return iface->node.key; +} + +void interface_add(const char *name, struct blob_attr *config, + struct blob_attr *devices); +void interface_free(struct interface *iface); +void interface_check_devices(void); +void interface_done(void); + +#endif diff --git a/feeds/ucentral/spotfilter/src/main.c b/feeds/ucentral/spotfilter/src/main.c new file mode 100644 index 000000000..1ac547972 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/main.c @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include + +#include + +#include "spotfilter.h" + +int spotfilter_run_cmd(char *cmd, bool ignore_error) +{ + char *argv[] = { "sh", "-c", cmd, NULL }; + bool first = true; + int status = -1; + char buf[512]; + int fds[2]; + FILE *f; + int pid; + + if (pipe(fds)) + return -1; + + pid = fork(); + if (!pid) { + close(fds[0]); + if (fds[1] != STDOUT_FILENO) + dup2(fds[1], STDOUT_FILENO); + if (fds[1] != STDERR_FILENO) + dup2(fds[1], STDERR_FILENO); + if (fds[1] > STDERR_FILENO) + close(fds[1]); + execv("/bin/sh", argv); + exit(1); + } + + if (pid < 0) + return -1; + + close(fds[1]); + f = fdopen(fds[0], "r"); + if (!f) { + close(fds[0]); + goto out; + } + + while (fgets(buf, sizeof(buf), f) != NULL) { + if (!strlen(buf)) + break; + if (ignore_error) + continue; + if (first) { + ULOG_WARN("Command: %s\n", cmd); + first = false; + } + ULOG_WARN("%s%s", buf, strchr(buf, '\n') ? "" : "\n"); + } + + fclose(f); + +out: + while (waitpid(pid, &status, 0) < 0) + if (errno != EINTR) + break; + + return status; +} + +static int usage(const char *progname) +{ + fprintf(stderr, "Usage: %s [options]\n" + "Options:\n" + "\n", progname); + + return 1; +} + +int main(int argc, char **argv) +{ + int ret = 2; + int ch; + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + return usage(argv[0]); + } + } + + ulog_open(ULOG_SYSLOG, LOG_DAEMON, "spotfilter"); + uloop_init(); + + if (rtnl_init()) + return 1; + + if (spotfilter_nl80211_init()) + return 1; + + if (spotfilter_dev_init()) + return 1; + + if (spotfilter_ubus_init()) + goto out; + + ret = 0; + uloop_run(); + + spotfilter_ubus_stop(); + +out: + interface_done(); + spotfilter_dev_done(); + spotfilter_nl80211_done(); + uloop_done(); + + return ret; +} diff --git a/feeds/ucentral/spotfilter/src/nl80211.c b/feeds/ucentral/spotfilter/src/nl80211.c new file mode 100644 index 000000000..2206b3be6 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/nl80211.c @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#define _GNU_SOURCE +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include "spotfilter.h" + +static struct nl_sock *genl; +static struct nl_cb *genl_cb; +static struct uloop_fd genl_fd; +static struct uloop_timeout update_timer; +static int nl80211_id; + +static int error_handler(struct sockaddr_nl *nla, struct nlmsgerr *err, + void *arg) +{ + int *ret = arg; + *ret = err->error; + return NL_STOP; +} + +static int ack_handler(struct nl_msg *msg, void *arg) +{ + int *ret = arg; + *ret = 0; + return NL_STOP; +} + +struct handler_args { + const char *group; + int id; +}; + +static int family_handler(struct nl_msg *msg, void *arg) +{ + struct handler_args *grp = arg; + struct nlattr *tb[CTRL_ATTR_MAX + 1]; + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + struct nlattr *mcgrp; + int rem_mcgrp; + + nla_parse(tb, CTRL_ATTR_MAX, genlmsg_attrdata(gnlh, 0), + genlmsg_attrlen(gnlh, 0), NULL); + + if (!tb[CTRL_ATTR_MCAST_GROUPS]) + return NL_SKIP; + + nla_for_each_nested(mcgrp, tb[CTRL_ATTR_MCAST_GROUPS], rem_mcgrp) { + struct nlattr *tb_mcgrp[CTRL_ATTR_MCAST_GRP_MAX + 1]; + + nla_parse(tb_mcgrp, CTRL_ATTR_MCAST_GRP_MAX, + nla_data(mcgrp), nla_len(mcgrp), NULL); + + if (!tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME] || + !tb_mcgrp[CTRL_ATTR_MCAST_GRP_ID]) + continue; + if (strncmp(nla_data(tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME]), + grp->group, nla_len(tb_mcgrp[CTRL_ATTR_MCAST_GRP_NAME]))) + continue; + grp->id = nla_get_u32(tb_mcgrp[CTRL_ATTR_MCAST_GRP_ID]); + break; + } + + return NL_SKIP; +} + +static int nl_get_multicast_id(struct nl_sock *sock, const char *family, const char *group) +{ + struct nl_msg *msg; + struct nl_cb *cb; + struct handler_args grp = { + .group = group, + .id = -ENOENT, + }; + int ret, ctrlid; + + msg = nlmsg_alloc(); + if (!msg) + return -ENOMEM; + + cb = nl_cb_alloc(NL_CB_DEFAULT); + if (!cb) { + ret = -ENOMEM; + goto out_fail_cb; + } + + ctrlid = genl_ctrl_resolve(sock, "nlctrl"); + + genlmsg_put(msg, 0, 0, ctrlid, 0, + 0, CTRL_CMD_GETFAMILY, 0); + + ret = -ENOBUFS; + NLA_PUT_STRING(msg, CTRL_ATTR_FAMILY_NAME, family); + + ret = nl_send_auto_complete(sock, msg); + if (ret < 0) + goto out; + + ret = 1; + + nl_cb_err(cb, NL_CB_CUSTOM, error_handler, &ret); + nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, ack_handler, &ret); + nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, family_handler, &grp); + + while (ret > 0) + nl_recvmsgs(sock, cb); + + if (ret == 0) + ret = grp.id; + nla_put_failure: + out: + nl_cb_put(cb); + out_fail_cb: + nlmsg_free(msg); + return ret; +} + +static void +nl80211_sock_cb(struct uloop_fd *fd, unsigned int events) +{ + nl_recvmsgs(genl, genl_cb); +} + +static void +nl80211_device_update(struct interface *iface, struct device *dev) +{ + struct nl_msg *msg; + + msg = nlmsg_alloc(); + genlmsg_put(msg, NL_AUTO_PID, NL_AUTO_SEQ, nl80211_id, 0, NLM_F_DUMP, + NL80211_CMD_GET_STATION, 0); + nla_put_u32(msg, NL80211_ATTR_IFINDEX, dev->ifindex); + + nl_send_auto_complete(genl, msg); + nlmsg_free(msg); +} + +static void +nl80211_interface_update(struct interface *iface) +{ + struct client *cl, *tmp; + struct device *dev; + + if (!iface->client_autoremove) + return; + + avl_for_each_element_safe(&iface->clients, cl, node, tmp) { + if (cl->idle++ < iface->client_timeout) + continue; + + client_free(iface, cl); + } + + vlist_for_each_element(&iface->devices, dev, node) + nl80211_device_update(iface, dev); +} + +static void spotfilter_nl80211_update(struct uloop_timeout *t) +{ + struct interface *iface; + + avl_for_each_element(&interfaces, iface, node) + nl80211_interface_update(iface); + + uloop_timeout_set(t, 1000); +} + +static int no_seq_check(struct nl_msg *msg, void *arg) +{ + return NL_OK; +} + +static int valid_msg(struct nl_msg *msg, void *arg) +{ + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + struct nlattr *tb[NL80211_ATTR_MAX + 1]; + struct interface *iface; + struct device *dev; + struct client *cl; + const void *addr; + int ifindex; + + nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), + genlmsg_attrlen(gnlh, 0), NULL); + + if (gnlh->cmd != NL80211_CMD_NEW_STATION) + return NL_SKIP; + + if (!tb[NL80211_ATTR_IFINDEX] || !tb[NL80211_ATTR_MAC]) + return NL_SKIP; + + ifindex = nla_get_u32(tb[NL80211_ATTR_IFINDEX]); + addr = nla_data(tb[NL80211_ATTR_MAC]); + + avl_for_each_element(&interfaces, iface, node) + vlist_for_each_element(&iface->devices, dev, node) + if (dev->ifindex == ifindex) + goto found; + + return NL_SKIP; + +found: + cl = avl_find_element(&iface->clients, addr, cl, node); + if (cl) + cl->idle = 0; + else if (iface->client_autocreate) + client_set(iface, addr, NULL, -1, -1, -1, NULL); + + return NL_SKIP; +} + +int spotfilter_nl80211_init(void) +{ + int id; + + genl = nl_socket_alloc(); + if (!genl) + return -1; + + nl_socket_set_buffer_size(genl, 16384, 16384); + if (genl_connect(genl)) + goto error; + + nl80211_id = genl_ctrl_resolve(genl, "nl80211"); + if (nl80211_id < 0) + goto error; + + id = nl_get_multicast_id(genl, "nl80211", "mlme"); + if (id < 0) + goto error; + + if (nl_socket_add_membership(genl, id) < 0) + goto error; + + genl_cb = nl_cb_alloc(NL_CB_DEFAULT); + nl_cb_set(genl_cb, NL_CB_SEQ_CHECK, NL_CB_CUSTOM, no_seq_check, NULL); + nl_cb_set(genl_cb, NL_CB_VALID, NL_CB_CUSTOM, valid_msg, NULL); + + genl_fd.fd = nl_socket_get_fd(genl); + genl_fd.cb = nl80211_sock_cb; + uloop_fd_add(&genl_fd, ULOOP_READ); + + update_timer.cb = spotfilter_nl80211_update; + uloop_timeout_set(&update_timer, 1); + + return 0; + +error: + spotfilter_nl80211_done(); + return -1; +} + +void spotfilter_nl80211_done(void) +{ + if (!genl) + return; + + uloop_timeout_cancel(&update_timer); + uloop_fd_delete(&genl_fd); + nl_socket_free(genl); + genl = NULL; +} diff --git a/feeds/ucentral/spotfilter/src/rtnl.c b/feeds/ucentral/spotfilter/src/rtnl.c new file mode 100644 index 000000000..a8e86a77e --- /dev/null +++ b/feeds/ucentral/spotfilter/src/rtnl.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "spotfilter.h" + +static struct nl_sock *rtnl; +bool rtnl_ignore_errors; + +static int +spotfilter_nl_error_cb(struct sockaddr_nl *nla, struct nlmsgerr *err, + void *arg) +{ + struct nlmsghdr *nlh = (struct nlmsghdr *) err - 1; + struct nlattr *tb[NLMSGERR_ATTR_MAX + 1]; + struct nlattr *attrs; + int ack_len = sizeof(*nlh) + sizeof(int) + sizeof(*nlh); + int len = nlh->nlmsg_len; + const char *errstr = "(unknown)"; + + if (rtnl_ignore_errors) + return NL_STOP; + + if (!(nlh->nlmsg_flags & NLM_F_ACK_TLVS)) + return NL_STOP; + + if (!(nlh->nlmsg_flags & NLM_F_CAPPED)) + ack_len += err->msg.nlmsg_len - sizeof(*nlh); + + attrs = (void *) ((unsigned char *) nlh + ack_len); + len -= ack_len; + + nla_parse(tb, NLMSGERR_ATTR_MAX, attrs, len, NULL); + if (tb[NLMSGERR_ATTR_MSG]) + errstr = nla_data(tb[NLMSGERR_ATTR_MSG]); + + fprintf(stderr, "Netlink error(%d): %s\n", err->error, errstr); + + return NL_STOP; +} + +int rtnl_call(struct nl_msg *msg) +{ + int ret; + + ret = nl_send_auto_complete(rtnl, msg); + nlmsg_free(msg); + + if (ret < 0) + return ret; + + return nl_wait_for_ack(rtnl); +} + +int rtnl_fd(void) +{ + return nl_socket_get_fd(rtnl); +} + +int rtnl_init(void) +{ + int fd, opt; + + if (rtnl) + return 0; + + rtnl = nl_socket_alloc(); + if (!rtnl) + return -1; + + if (nl_connect(rtnl, NETLINK_ROUTE)) + goto free; + + nl_socket_disable_seq_check(rtnl); + nl_socket_set_buffer_size(rtnl, 65536, 0); + nl_cb_err(nl_socket_get_cb(rtnl), NL_CB_CUSTOM, spotfilter_nl_error_cb, NULL); + + fd = nl_socket_get_fd(rtnl); + + opt = 1; + setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &opt, sizeof(opt)); + + opt = 1; + setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &opt, sizeof(opt)); + + return 0; + +free: + nl_socket_free(rtnl); + rtnl = NULL; + return -1; +} diff --git a/feeds/ucentral/spotfilter/src/snoop.c b/feeds/ucentral/spotfilter/src/snoop.c new file mode 100644 index 000000000..266de5236 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/snoop.c @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "spotfilter.h" + +#define FLAG_RESPONSE 0x8000 +#define FLAG_OPCODE 0x7800 +#define FLAG_AUTHORATIVE 0x0400 +#define FLAG_RCODE 0x000f + +#define TYPE_A 0x0001 +#define TYPE_CNAME 0x0005 +#define TYPE_PTR 0x000c +#define TYPE_TXT 0x0010 +#define TYPE_AAAA 0x001c +#define TYPE_SRV 0x0021 +#define TYPE_ANY 0x00ff + +#define IS_COMPRESSED(x) ((x & 0xc0) == 0xc0) + +#define CLASS_FLUSH 0x8000 +#define CLASS_UNICAST 0x8000 +#define CLASS_IN 0x0001 + +#define MAX_NAME_LEN 256 +#define MAX_DATA_LEN 8096 + +int spotfilter_ifb_ifindex; +static struct uloop_fd ufd; +static struct uloop_timeout cname_gc_timer; + +struct vlan_hdr { + uint16_t tci; + uint16_t proto; +}; + +struct packet { + void *head; + void *buffer; + unsigned int len; +}; + +struct dns_header { + uint16_t id; + uint16_t flags; + uint16_t questions; + uint16_t answers; + uint16_t authority; + uint16_t additional; +} __packed; + +struct dns_question { + uint16_t type; + uint16_t class; +} __packed; + +struct dns_answer { + uint16_t type; + uint16_t class; + uint32_t ttl; + uint16_t rdlength; +} __packed; + +struct addr_entry_data { + union { + struct { + uint32_t _pad; + uint32_t ip4addr; + }; + uint32_t ip6addr[4]; + }; + uint32_t timeout; +}; + +struct addr_entry { + struct avl_node node; + struct addr_entry_data data; +}; + +struct cname_entry { + struct avl_node node; + uint8_t class; + uint8_t age; +}; + +static uint32_t spotfilter_gettime(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return ts.tv_sec; +} + + +static void * +pkt_peek(struct packet *pkt, unsigned int len) +{ + if (len > pkt->len) + return NULL; + + return pkt->buffer; +} + + +static void * +pkt_pull(struct packet *pkt, unsigned int len) +{ + void *ret = pkt_peek(pkt, len); + + if (!ret) + return NULL; + + pkt->buffer += len; + pkt->len -= len; + + return ret; +} + +static bool +proto_is_vlan(uint16_t proto) +{ + return proto == ETH_P_8021Q || proto == ETH_P_8021AD; +} + +static int pkt_pull_name(struct packet *pkt, const void *hdr, char *dest) +{ + int len; + + if (dest) + len = dn_expand(hdr, pkt->buffer + pkt->len, pkt->buffer, + (void *)dest, MAX_NAME_LEN); + else + len = dn_skipname(pkt->buffer, pkt->buffer + pkt->len - 1); + + if (len < 0 || !pkt_pull(pkt, len)) + return -1; + + return 0; +} + +static void +cname_cache_set(struct interface *iface, const char *name, int class) +{ + struct cname_entry *e; + + if (class < 0) + return; + + e = avl_find_element(&iface->cname_cache, name, e, node); + if (!e) { + char *name_buf; + + e = calloc_a(sizeof(*e), &name_buf, strlen(name) + 1); + e->node.key = strcpy(name_buf, name); + avl_insert(&iface->cname_cache, &e->node); + } + + e->age = 0; + e->class = (uint8_t)class; +} + +static int +cname_cache_get(struct interface *iface, const char *name, int *class) +{ + struct cname_entry *e; + + e = avl_find_element(&iface->cname_cache, name, e, node); + if (!e) + return -1; + + if (*class < 0) + *class = e->class; + + return 0; +} + +static bool +__spotfilter_dns_whitelist_lookup(struct blob_attr *attr, const char *name, int *class) +{ + enum { + WL_ATTR_CLASS, + WL_ATTR_HOSTS, + __WL_ATTR_MAX + }; + static const struct blobmsg_policy policy[__WL_ATTR_MAX] = { + [WL_ATTR_CLASS] = { "class", BLOBMSG_TYPE_INT32 }, + [WL_ATTR_HOSTS] = { "hosts", BLOBMSG_TYPE_ARRAY }, + }; + struct blob_attr *tb[__WL_ATTR_MAX]; + struct blob_attr *cur; + int rem; + + blobmsg_parse(policy, __WL_ATTR_MAX, tb, blobmsg_data(attr), blobmsg_len(attr)); + + if (!tb[WL_ATTR_CLASS] || !tb[WL_ATTR_HOSTS]) + return false; + + blobmsg_for_each_attr(cur, tb[WL_ATTR_HOSTS], rem) { + if (fnmatch(blobmsg_get_string(cur), name, 0)) + continue; + + *class = blobmsg_get_u32(tb[WL_ATTR_CLASS]); + return true; + } + + return false; +} + +static void +spotfilter_dns_whitelist_lookup(struct interface *iface, const char *name, int *class) +{ + struct blob_attr *cur; + int rem; + + if (!iface->whitelist) + return; + + blobmsg_for_each_attr(cur, iface->whitelist, rem) { + if (__spotfilter_dns_whitelist_lookup(cur, name, class)) + return; + } +} + +static void +spotfilter_dns_whitelist_map_add(struct interface *iface, const struct addr_entry_data *data, + bool ipv6, int class) +{ + struct addr_entry *e; + uint8_t val = (uint8_t)class; + int32_t delta; + + if (class < 0) + return; + + e = avl_find_element(&iface->addr_map, data, e, node); + if (!e) { + e = calloc(1, sizeof(*e)); + memcpy(&e->data, data, sizeof(e->data)); + e->node.key = &e->data; + avl_insert(&iface->addr_map, &e->node); + } + + spotfilter_bpf_set_whitelist(iface, ipv6 ? data->ip6addr : &data->ip4addr, ipv6, &val); + e->data.timeout = spotfilter_gettime() + data->timeout; + + delta = e->data.timeout - iface->next_gc; + if (iface->next_gc && delta < 0) + uloop_timeout_set(&iface->addr_gc, data->timeout); +} + +static int +dns_parse_question(struct interface *iface, struct packet *pkt, const void *hdr, int *class) +{ + char qname[MAX_NAME_LEN]; + + if (pkt_pull_name(pkt, hdr, qname) || + !pkt_pull(pkt, sizeof(struct dns_question))) + return -1; + + cname_cache_get(iface, qname, class); + spotfilter_dns_whitelist_lookup(iface, qname, class); + + return 0; +} + +static int +dns_parse_answer(struct interface *iface, struct packet *pkt, void *hdr, int *class) +{ + char cname[MAX_NAME_LEN]; + struct dns_answer *a; + struct addr_entry_data data = {}; + bool ipv6 = false; + void *rdata; + int len; + + if (pkt_pull_name(pkt, hdr, NULL)) + return -1; + + a = pkt_pull(pkt, sizeof(*a)); + if (!a) + return -1; + + len = be16_to_cpu(a->rdlength); + rdata = pkt_pull(pkt, len); + if (!rdata) + return -1; + + switch (be16_to_cpu(a->type)) { + case TYPE_CNAME: + if (dn_expand(hdr, pkt->buffer + pkt->len, rdata, + cname, sizeof(cname)) < 0) + return -1; + + spotfilter_dns_whitelist_lookup(iface, cname, class); + cname_cache_set(iface, cname, *class); + return 0; + case TYPE_A: + memcpy(&data.ip4addr, rdata, 4); + if (!data.ip4addr) + return 0; + break; + case TYPE_AAAA: + ipv6 = true; + memcpy(&data.ip6addr, rdata, 16); + if (!data.ip6addr[0]) + return 0; + break; + default: + return 0; + } + + if (class < 0) + return 0; + + data.timeout = be32_to_cpu(a->ttl); + spotfilter_dns_whitelist_map_add(iface, &data, ipv6, *class); + + return 0; +} + +static void +spotfilter_dns_iface_recv(struct interface *iface, struct packet *pkt) +{ + struct dns_header *h; + int class = -1; + int i; + + h = pkt_pull(pkt, sizeof(*h)); + if (!h) + return; + + if ((h->flags & cpu_to_be16(FLAG_RESPONSE | FLAG_OPCODE | FLAG_RCODE)) != + cpu_to_be16(FLAG_RESPONSE)) + return; + + if (h->questions != cpu_to_be16(1)) + return; + + if (dns_parse_question(iface, pkt, h, &class)) + return; + + for (i = 0; i < be16_to_cpu(h->answers); i++) + if (dns_parse_answer(iface, pkt, h, &class)) + return; +} + +static void +spotfilter_dns_recv(struct packet *pkt) +{ + struct interface *iface; + + avl_for_each_element(&interfaces, iface, node) { + struct packet tmp_pkt = *pkt; + + spotfilter_dns_iface_recv(iface, &tmp_pkt); + } +} + +static void +spotfilter_parse_udp_v4(struct packet *pkt, uint16_t src_port, uint16_t dst_port) +{ + struct ethhdr *eth = pkt->head; + + if (src_port != 67 || dst_port != 68) + return; + + spotfilter_recv_dhcpv4(pkt->buffer, pkt->len, eth->h_dest); +} + +static void +spotfilter_packet_cb(struct packet *pkt) +{ + uint16_t proto, src_port, dst_port; + struct ethhdr *eth; + struct ip6_hdr *ip6; + struct ip *ip; + struct udphdr *udp; + bool ipv4; + + eth = pkt_pull(pkt, sizeof(*eth)); + if (!eth) + return; + + proto = be16_to_cpu(eth->h_proto); + if (proto_is_vlan(proto)) { + struct vlan_hdr *vlan; + + vlan = pkt_pull(pkt, sizeof(*vlan)); + if (!vlan) + return; + + proto = be16_to_cpu(vlan->proto); + } + + switch (proto) { + case ETH_P_IP: + ip = pkt_peek(pkt, sizeof(struct ip)); + if (!ip) + return; + + if (!pkt_pull(pkt, ip->ip_hl * 4)) + return; + + proto = ip->ip_p; + ipv4 = true; + break; + case ETH_P_IPV6: + ip6 = pkt_pull(pkt, sizeof(*ip6)); + if (!ip6) + return; + + proto = ip6->ip6_nxt; + if (proto == IPPROTO_ICMPV6) { + if (ip6->ip6_hlim != 255) + return; + + spotfilter_recv_icmpv6(pkt->buffer, pkt->len, eth->h_source, eth->h_dest); + return; + } + break; + default: + return; + } + + if (proto != IPPROTO_UDP) + return; + + udp = pkt_pull(pkt, sizeof(struct udphdr)); + if (!udp) + return; + + src_port = ntohs(udp->uh_sport); + dst_port = ntohs(udp->uh_dport); + + if (ipv4) + spotfilter_parse_udp_v4(pkt, src_port, dst_port); + + if (src_port == 53) + spotfilter_dns_recv(pkt); +} + +static void +spotfilter_socket_cb(struct uloop_fd *fd, unsigned int events) +{ + static uint8_t buf[8192]; + struct packet pkt = { + .head = buf, + .buffer = buf, + }; + int len; + +retry: + len = recvfrom(fd->fd, buf, sizeof(buf), MSG_DONTWAIT, NULL, NULL); + if (len < 0) { + if (errno == EINTR) + goto retry; + return; + } + + if (!len) + return; + + pkt.len = len; + spotfilter_packet_cb(&pkt); +} + +static int +spotfilter_open_socket(void) +{ + struct sockaddr_ll sll = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_ALL), + }; + int sock; + + sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (sock == -1) { + ULOG_ERR("failed to create raw socket: %s\n", strerror(errno)); + return -1; + } + + sll.sll_ifindex = if_nametoindex(SPOTFILTER_IFB_NAME); + if (bind(sock, (struct sockaddr *)&sll, sizeof(sll))) { + ULOG_ERR("failed to bind socket to "SPOTFILTER_IFB_NAME": %s\n", + strerror(errno)); + goto error; + } + + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL) | O_NONBLOCK); + + ufd.fd = sock; + ufd.cb = spotfilter_socket_cb; + uloop_fd_add(&ufd, ULOOP_READ); + + return 0; + +error: + close(sock); + return -1; +} + + +static void +spotfilter_addr_gc(struct uloop_timeout *t) +{ + struct interface *iface = container_of(t, struct interface, addr_gc); + struct addr_entry *e, *tmp; + uint32_t now = spotfilter_gettime(); + int32_t timeout = 0; + + iface->next_gc = 0; + avl_for_each_element_safe(&iface->addr_map, e, node, tmp) { + const void *addr = e->data.ip6addr[0] ? &e->data.ip6addr[0] : &e->data.ip4addr; + bool ipv6 = !!e->data.ip6addr[0]; + int32_t cur_timeout; + + cur_timeout = e->data.timeout - now; + if (cur_timeout <= 0) { + if (!spotfilter_bpf_whitelist_seen(iface, addr, ipv6)) { + spotfilter_bpf_set_whitelist(iface, addr, ipv6, NULL); + avl_delete(&iface->addr_map, &e->node); + free(e); + continue; + } + + e->data.timeout = now + iface->active_timeout; + } + + if (!timeout || cur_timeout < timeout) { + timeout = cur_timeout; + iface->next_gc = e->data.timeout; + } + } + + if (!timeout) + return; + + uloop_timeout_set(&iface->addr_gc, timeout * 1000); +} + +static void +spotfilter_cname_cache_gc(struct uloop_timeout *timeout) +{ + struct interface *iface; + struct cname_entry *e, *tmp; + + avl_for_each_element(&interfaces, iface, node) { + avl_for_each_element_safe(&iface->cname_cache, e, node, tmp) { + if (e->age++ < 5) + continue; + + avl_delete(&iface->cname_cache, &e->node); + free(e); + } + } + + uloop_timeout_set(timeout, 1000); +} + +static int avl_addr_cmp(const void *k1, const void *k2, void *ptr) +{ + return memcmp(k1, k2, 16); +} + + +void spotfilter_dns_init(struct interface *iface) +{ + avl_init(&iface->cname_cache, avl_strcmp, false, NULL); + avl_init(&iface->addr_map, avl_addr_cmp, false, NULL); + iface->addr_gc.cb = spotfilter_addr_gc; +} + +void spotfilter_dns_free(struct interface *iface) +{ + struct cname_entry *e, *tmp; + + avl_remove_all_elements(&iface->cname_cache, e, node, tmp) + free(e); +} + +int spotfilter_dev_init(void) +{ + cname_gc_timer.cb = spotfilter_cname_cache_gc; + spotfilter_cname_cache_gc(&cname_gc_timer); + + spotfilter_dev_done(); + + if (spotfilter_run_cmd("ip link add "SPOTFILTER_IFB_NAME" type ifb", false) || + spotfilter_run_cmd("ip link set dev "SPOTFILTER_IFB_NAME" up", false) || + spotfilter_open_socket()) + return -1; + + spotfilter_ifb_ifindex = if_nametoindex(SPOTFILTER_IFB_NAME); + + return 0; +} + +void spotfilter_dev_done(void) +{ + if (ufd.registered) { + uloop_fd_delete(&ufd); + close(ufd.fd); + } + + spotfilter_run_cmd("ip link del "SPOTFILTER_IFB_NAME, true); +} diff --git a/feeds/ucentral/spotfilter/src/spotfilter-bpf.c b/feeds/ucentral/spotfilter/src/spotfilter-bpf.c new file mode 100644 index 000000000..6b969551c --- /dev/null +++ b/feeds/ucentral/spotfilter/src/spotfilter-bpf.c @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#define KBUILD_MODNAME "foo" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "bpf_skb_utils.h" +#include "spotfilter-bpf.h" + +static const volatile struct spotfilter_bpf_config config = {}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(key_size, sizeof(uint32_t)); + __type(value, struct spotfilter_bpf_class); + __uint(max_entries, SPOTFILTER_NUM_CLASS); +} class SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(key_size, sizeof(struct spotfilter_client_key)); + __type(value, struct spotfilter_client_data); + __uint(max_entries, 1000); + __uint(map_flags, BPF_F_NO_PREALLOC); +} client SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(key_size, sizeof(struct in_addr)); + __type(value, struct spotfilter_whitelist_entry); + __uint(max_entries, 10000); + __uint(map_flags, BPF_F_NO_PREALLOC); +} whitelist_ipv4 SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(key_size, sizeof(struct in6_addr)); + __type(value, struct spotfilter_whitelist_entry); + __uint(max_entries, 10000); + __uint(map_flags, BPF_F_NO_PREALLOC); +} whitelist_ipv6 SEC(".maps"); + +static bool +is_dhcpv4_port(uint16_t port) +{ + return port == bpf_htons(67) || port == bpf_htons(68); +} + +static __always_inline bool +check_ipv4_control(struct skb_parser_info *info) +{ + struct udphdr *udph; + + if (info->proto != IPPROTO_UDP) + return false; + + udph = skb_info_ptr(info, sizeof(*udph)); + if (!udph) + return false; + + return is_dhcpv4_port(udph->source) && is_dhcpv4_port(udph->dest); +} + +static bool +is_dhcpv6_port(uint16_t port) +{ + return port == bpf_htons(546) || port == bpf_htons(547); +} + +static bool +is_icmpv6_control(uint8_t type) +{ + switch (type) { + case ICMPV6_PKT_TOOBIG: + case NDISC_ROUTER_SOLICITATION: + case NDISC_ROUTER_ADVERTISEMENT: + case NDISC_NEIGHBOUR_SOLICITATION: + case NDISC_NEIGHBOUR_ADVERTISEMENT: + case NDISC_REDIRECT: + case ICMPV6_MGM_QUERY: + case ICMPV6_MGM_REPORT: + return true; + default: + return false; + } +} + +static __always_inline bool +check_ipv6_control(struct skb_parser_info *info) +{ + if (info->proto == IPPROTO_UDP) { + struct udphdr *udph; + + udph = skb_info_ptr(info, sizeof(*udph)); + if (!udph) + return false; + + return is_dhcpv6_port(udph->source) && is_dhcpv6_port(udph->dest); + } + + if (info->proto == IPPROTO_ICMPV6) { + struct icmp6hdr *icmp6h; + + icmp6h = skb_info_ptr(info, sizeof(*icmp6h)); + if (!icmp6h) + return false; + + return is_icmpv6_control(icmp6h->icmp6_type); + } + + return false; +} + +static __always_inline bool +check_dns(struct skb_parser_info *info, bool ingress) +{ + struct udphdr *udph; + + if (info->proto != IPPROTO_UDP) + return false; + + udph = skb_info_ptr(info, sizeof(*udph)); + if (!udph) + return false; + + if (ingress) + return udph->dest == bpf_htons(53); + + return udph->source == bpf_htons(53); +} + +SEC("tc/egress") +int spotfilter_out(struct __sk_buff *skb) +{ + struct spotfilter_client_data *cl; + struct skb_parser_info info; + struct ethhdr *eth; + bool is_control = false; + bool is_dns = false; + + skb_parse_init(&info, skb); + eth = skb_parse_ethernet(&info); + if (!eth) + return TC_ACT_UNSPEC; + + cl = bpf_map_lookup_elem(&client, eth->h_dest); + if (cl) { + if (cl->flags & SPOTFILTER_CLIENT_F_ACCT_DL) + cl->bytes_dl += skb->len; + } + + skb_parse_vlan(&info); + if (skb_parse_ipv4(&info, sizeof(struct udphdr))) { + is_control = check_ipv4_control(&info); + is_dns = check_dns(&info, false); + } else if (skb_parse_ipv6(&info, sizeof(struct icmp6hdr))) { + is_control = check_ipv6_control(&info); + is_dns = check_dns(&info, false); + } else { + return TC_ACT_UNSPEC; + } + + if (is_control || is_dns) + bpf_clone_redirect(skb, config.snoop_ifindex, BPF_F_INGRESS); + + return TC_ACT_UNSPEC; +} + +SEC("tc/ingress") +int spotfilter_in(struct __sk_buff *skb) +{ + struct spotfilter_client_data *cl, cldata = {}; + struct spotfilter_bpf_class *c, cdata; + struct skb_parser_info info; + struct ipv6hdr *ip6h; + struct ethhdr *eth; + struct iphdr *iph; + bool addr_match = false; + bool is_control = false; + bool has_vlan = false; + bool is_dns = false; + struct spotfilter_whitelist_entry *wl_val = NULL; + uint32_t cur_class; + + skb_parse_init(&info, skb); + eth = skb_parse_ethernet(&info); + if (!eth) + return TC_ACT_UNSPEC; + + cl = bpf_map_lookup_elem(&client, eth->h_source); + if (cl) { + cldata = *cl; + if (cl->flags & SPOTFILTER_CLIENT_F_ACCT_UL) + cl->bytes_ul += skb->len; + } + + has_vlan = !!skb_parse_vlan(&info); + if ((iph = skb_parse_ipv4(&info, sizeof(struct udphdr))) != NULL) { + addr_match = iph->saddr == cldata.ip4addr; + is_control = check_ipv4_control(&info); + is_dns = check_dns(&info, true); + + if (!is_control) + wl_val = bpf_map_lookup_elem(&whitelist_ipv4, &iph->daddr); + } else if ((ip6h = skb_parse_ipv6(&info, sizeof(struct icmp6hdr))) != NULL) { + addr_match = ipv6_addr_equal(&ip6h->saddr, (struct in6_addr *)&cldata.ip6addr); + if ((ip6h->saddr.s6_addr[0] & 0xe0) != 0x20) + addr_match = true; + is_control = check_ipv6_control(&info); + is_dns = check_dns(&info, true); + + if (!is_control) + wl_val = bpf_map_lookup_elem(&whitelist_ipv6, &ip6h->daddr); + } else { + return TC_ACT_UNSPEC; + } + + if (wl_val) { + cldata.cur_class = wl_val->val; + cldata.dns_class = wl_val->val; + wl_val->seen = 1; + } + + if (is_control) { + bpf_clone_redirect(skb, config.snoop_ifindex, BPF_F_INGRESS); + return TC_ACT_UNSPEC; + } + + if (!addr_match) { + if (!is_control) + return TC_ACT_SHOT; + + memset(&cldata, 0, sizeof(cldata)); + } + + cur_class = is_dns ? cldata.dns_class : cldata.cur_class; + c = bpf_map_lookup_elem(&class, &cur_class); + if (c) + cdata = *c; + else + return TC_ACT_UNSPEC; + + if (!(cdata.actions & SPOTFILTER_ACTION_VALID)) + return TC_ACT_SHOT; + + if (cdata.actions & SPOTFILTER_ACTION_SET_DEST_MAC) { + eth = skb_ptr(skb, 0, sizeof(*eth)); + if (!eth) + return TC_ACT_UNSPEC; + + memcpy(eth->h_dest, cdata.dest_mac, ETH_ALEN); + } + + if (cdata.actions & SPOTFILTER_ACTION_FWMARK) + skb->mark = (skb->mark & ~cdata.fwmark_mask) | cdata.fwmark_val; + + if (cdata.actions & SPOTFILTER_ACTION_REDIRECT) { + if (cdata.actions & SPOTFILTER_ACTION_REDIRECT_VLAN) { + if (has_vlan && bpf_skb_vlan_pop(skb)) + return -1; + + if (cdata.redirect_vlan_proto && + bpf_skb_vlan_push(skb, cdata.redirect_vlan_proto, cdata.redirect_vlan)) + return -1; + } + + return bpf_redirect(cdata.redirect_ifindex, 0); + } + + return TC_ACT_UNSPEC; +} + +char _license[] SEC("license") = "GPL"; diff --git a/feeds/ucentral/spotfilter/src/spotfilter-bpf.h b/feeds/ucentral/spotfilter/src/spotfilter-bpf.h new file mode 100644 index 000000000..d9f4f08f3 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/spotfilter-bpf.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __BPF_SPOTFILTER_H +#define __BPF_SPOTFILTER_H + +struct spotfilter_client_key { + uint8_t addr[6]; +}; + +#define SPOTFILTER_CLIENT_F_ACCT_UL (1 << 0) +#define SPOTFILTER_CLIENT_F_ACCT_DL (1 << 1) + +struct spotfilter_client_data { + uint32_t ip4addr; + uint32_t ip6addr[4]; + uint8_t cur_class; + uint8_t dns_class; + uint8_t flags; + + uint64_t bytes_ul; + uint64_t bytes_dl; +}; + +struct spotfilter_bpf_config { + uint32_t snoop_ifindex; +}; + +struct spotfilter_whitelist_entry { + uint8_t val; + uint8_t seen; +}; + +#define SPOTFILTER_NUM_CLASS 16 + +#define SPOTFILTER_ACTION_FWMARK (1 << 0) +#define SPOTFILTER_ACTION_REDIRECT (1 << 1) +#define SPOTFILTER_ACTION_REDIRECT_VLAN (1 << 2) +#define SPOTFILTER_ACTION_SET_DEST_MAC (1 << 3) + +#define SPOTFILTER_ACTION_VALID (1 << 15) + + +struct spotfilter_bpf_class { + uint16_t actions; + uint8_t dest_mac[6]; + + uint32_t fwmark_val; + uint32_t fwmark_mask; + + uint32_t redirect_ifindex; + uint16_t redirect_vlan; + uint16_t redirect_vlan_proto; +}; + +#endif diff --git a/feeds/ucentral/spotfilter/src/spotfilter.h b/feeds/ucentral/spotfilter/src/spotfilter.h new file mode 100644 index 000000000..d1cfb50f3 --- /dev/null +++ b/feeds/ucentral/spotfilter/src/spotfilter.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#ifndef __SPOTFILTER_H +#define __SPOTFILTER_H + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "spotfilter-bpf.h" +#include "interface.h" +#include "bpf.h" +#include "client.h" + +#define SPOTFILTER_IFB_NAME "spotfilter-ifb" + +#define SPOTFILTER_PROG_PATH "/lib/bpf/spotfilter-bpf.o" + +#define SPOTFILTER_PRIO_BASE 0x120 + +extern int spotfilter_ifb_ifindex; +struct nl_msg; + +int rtnl_init(void); +int rtnl_fd(void); +int rtnl_call(struct nl_msg *msg); + +int spotfilter_run_cmd(char *cmd, bool ignore_error); + +int spotfilter_ubus_init(void); +void spotfilter_ubus_stop(void); +void spotfilter_ubus_notify(struct interface *iface, struct client *cl, const char *type); + +int spotfilter_dev_init(void); +void spotfilter_dev_done(void); + +void spotfilter_dns_init(struct interface *iface); +void spotfilter_dns_free(struct interface *iface); + +void spotfilter_recv_dhcpv4(const void *msg, int len, const void *eth_addr); +void spotfilter_recv_icmpv6(const void *data, int len, const uint8_t *src, const uint8_t *dest); + +int spotfilter_nl80211_init(void); +void spotfilter_nl80211_done(void); + +#endif diff --git a/feeds/ucentral/spotfilter/src/ubus.c b/feeds/ucentral/spotfilter/src/ubus.c new file mode 100644 index 000000000..8083c589f --- /dev/null +++ b/feeds/ucentral/spotfilter/src/ubus.c @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Felix Fietkau + */ +#include +#include +#include +#include + +#include "spotfilter.h" + +static struct blob_buf b; + +enum { + IFACE_ATTR_NAME, + IFACE_ATTR_CONFIG, + IFACE_ATTR_DEVICES, + __IFACE_ATTR_MAX, +}; + +static const struct blobmsg_policy iface_policy[__IFACE_ATTR_MAX] = { + [IFACE_ATTR_NAME] = { "name", BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_CONFIG] = { "config", BLOBMSG_TYPE_TABLE }, + [IFACE_ATTR_DEVICES] = { "devices", BLOBMSG_TYPE_ARRAY }, +}; + +static int +interface_ubus_add(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__IFACE_ATTR_MAX]; + struct blob_attr *cur; + const char *name; + + blobmsg_parse(iface_policy, __IFACE_ATTR_MAX, tb, blobmsg_data(msg), blobmsg_len(msg)); + + if ((cur = tb[IFACE_ATTR_NAME]) != NULL) + name = blobmsg_get_string(tb[IFACE_ATTR_NAME]); + else + return UBUS_STATUS_INVALID_ARGUMENT; + + if ((cur = tb[IFACE_ATTR_DEVICES]) != NULL && + blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) < 0) + return UBUS_STATUS_INVALID_ARGUMENT; + + interface_add(name, tb[IFACE_ATTR_CONFIG], tb[IFACE_ATTR_DEVICES]); + + return 0; +} + +static int +interface_ubus_remove(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct interface *iface; + struct blob_attr *tb; + + blobmsg_parse(&iface_policy[IFACE_ATTR_NAME], 1, &tb, + blobmsg_data(msg), blobmsg_len(msg)); + + if (tb) + return UBUS_STATUS_INVALID_ARGUMENT; + + iface = avl_find_element(&interfaces, blobmsg_get_string(tb), iface, node); + if (!iface) + return UBUS_STATUS_NOT_FOUND; + + interface_free(iface); + return 0; +} + +static int +check_devices(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + interface_check_devices(); + return 0; +} + +enum { + CLIENT_ATTR_IFACE, + CLIENT_ATTR_ADDR, + CLIENT_ATTR_ID, + CLIENT_ATTR_STATE, + CLIENT_ATTR_DNS_STATE, + CLIENT_ATTR_ACCOUNTING, + CLIENT_ATTR_DATA, + __CLIENT_ATTR_MAX +}; + +static const struct blobmsg_policy client_policy[__CLIENT_ATTR_MAX] = { + [CLIENT_ATTR_IFACE] = { "interface", BLOBMSG_TYPE_STRING }, + [CLIENT_ATTR_ADDR] = { "address", BLOBMSG_TYPE_STRING }, + [CLIENT_ATTR_ID] = { "id", BLOBMSG_TYPE_STRING }, + [CLIENT_ATTR_STATE] = { "state", BLOBMSG_TYPE_INT32 }, + [CLIENT_ATTR_DNS_STATE] = { "dns_state", BLOBMSG_TYPE_INT32 }, + [CLIENT_ATTR_ACCOUNTING] = { "accounting", BLOBMSG_TYPE_ARRAY }, + [CLIENT_ATTR_DATA] = { "data", BLOBMSG_TYPE_TABLE }, +}; + +static int +client_ubus_init(struct blob_attr *msg, struct blob_attr **tb, + struct interface **iface, const void **addr, + const char **id, struct client **cl) +{ + struct blob_attr *cur; + + blobmsg_parse(client_policy, __CLIENT_ATTR_MAX, tb, + blobmsg_data(msg), blobmsg_len(msg)); + + if ((cur = tb[CLIENT_ATTR_IFACE]) != NULL) + *iface = avl_find_element(&interfaces, blobmsg_get_string(cur), + *iface, node); + else + return UBUS_STATUS_INVALID_ARGUMENT; + + if (!*iface) + return UBUS_STATUS_NOT_FOUND; + + if ((cur = tb[CLIENT_ATTR_ADDR]) != NULL) + *addr = ether_aton(blobmsg_get_string(cur)); + else + *addr = NULL; + + if ((cur = tb[CLIENT_ATTR_ID]) != NULL) + *id = blobmsg_get_string(cur); + else + *id = NULL; + + if (*addr) + *cl = avl_find_element(&(*iface)->clients, *addr, *cl, node); + else if (*id) + *cl = avl_find_element(&(*iface)->client_ids, *id, *cl, id_node); + else + return UBUS_STATUS_INVALID_ARGUMENT; + + if (*cl && !*addr) + *addr = (*cl)->node.key; + + return 0; +} + +static int +client_accounting_flags(struct blob_attr *attr) +{ + struct blob_attr *cur; + int flags = 0; + int rem; + + blobmsg_for_each_attr(cur, attr, rem) { + const char *val = blobmsg_get_string(cur); + + if (!strcmp(val, "ul")) + flags |= SPOTFILTER_CLIENT_F_ACCT_UL; + else if (!strcmp(val, "dl")) + flags |= SPOTFILTER_CLIENT_F_ACCT_DL; + } + + return flags; +} + + +static int +client_ubus_update(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__CLIENT_ATTR_MAX]; + struct interface *iface = NULL; + struct blob_attr *cur; + struct client *cl = NULL; + const void *addr = NULL; + const char *id = NULL; + int state = -1, dns_state = -1; + int accounting = -1; + int ret; + + ret = client_ubus_init(msg, tb, &iface, &addr, &id, &cl); + if (ret) + return ret; + + if ((cur = tb[CLIENT_ATTR_STATE]) != NULL) + dns_state = state = blobmsg_get_u32(cur); + + if ((cur = tb[CLIENT_ATTR_DNS_STATE]) != NULL) + dns_state = blobmsg_get_u32(cur); + + if ((cur = tb[CLIENT_ATTR_ACCOUNTING]) != NULL && + blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) >= 0) + accounting = client_accounting_flags(cur); + + if (!strcmp(method, "client_remove")) { + if (!cl) + return UBUS_STATUS_NOT_FOUND; + + client_free(iface, cl); + return 0; + } + + if (!addr) + return UBUS_STATUS_INVALID_ARGUMENT; + + client_set(iface, addr, id, state, dns_state, accounting, + tb[CLIENT_ATTR_DATA]); + + return 0; +} + +static void +interface_dump_action(struct blob_buf *buf, struct interface *iface, uint8_t class) +{ + struct spotfilter_bpf_class *c = &iface->cdata[class]; + char ifname[IFNAMSIZ + 1]; + + if (!(c->actions & SPOTFILTER_ACTION_VALID)) { + blobmsg_add_u8(buf, "invalid", 1); + return; + } + + if (c->actions & SPOTFILTER_ACTION_FWMARK) { + blobmsg_add_u32(buf, "fwmark", c->fwmark_val); + blobmsg_add_u32(buf, "fwmark_mask", c->fwmark_mask); + } + + if (c->actions & SPOTFILTER_ACTION_REDIRECT) + blobmsg_add_string(buf, "redirect", if_indextoname(c->redirect_ifindex, ifname)); + + if (c->actions & SPOTFILTER_ACTION_SET_DEST_MAC) + blobmsg_add_string(buf, "dest_mac", ether_ntoa((const void *)c->dest_mac)); +} + +static void client_dump(struct interface *iface, struct client *cl) +{ + struct blob_attr *val; + const char *name; + char *buf; + void *c; + + spotfilter_bpf_get_client(iface, &cl->key, &cl->data); + + if (iface->client_autoremove) + blobmsg_add_u32(&b, "idle", cl->idle); + + blobmsg_add_u32(&b, "state", cl->data.cur_class); + blobmsg_add_u32(&b, "dns_state", cl->data.dns_class); + if (cl->id_node.key) + blobmsg_add_string(&b, "id", (const char *)cl->id_node.key); + + if (cl->data.ip4addr) { + buf = blobmsg_alloc_string_buffer(&b, "ip4addr", INET6_ADDRSTRLEN); + inet_ntop(AF_INET, (const void *)&cl->data.ip4addr, buf, INET6_ADDRSTRLEN); + blobmsg_add_string_buffer(&b); + } + + if (cl->data.ip6addr[0]) { + buf = blobmsg_alloc_string_buffer(&b, "ip6addr", INET6_ADDRSTRLEN); + inet_ntop(AF_INET6, (const void *)cl->data.ip6addr, buf, INET6_ADDRSTRLEN); + blobmsg_add_string_buffer(&b); + } + + c = blobmsg_open_array(&b, "accounting"); + if (cl->data.flags & SPOTFILTER_CLIENT_F_ACCT_UL) + blobmsg_add_string(&b, NULL, "ul"); + if (cl->data.flags & SPOTFILTER_CLIENT_F_ACCT_DL) + blobmsg_add_string(&b, NULL, "dl"); + blobmsg_close_table(&b, c); + + c = blobmsg_open_table(&b, "data"); + kvlist_for_each(&cl->kvdata, name, val) + blobmsg_add_blob(&b, val); + blobmsg_close_table(&b, c); + + c = blobmsg_open_table(&b, "action"); + interface_dump_action(&b, iface, cl->data.cur_class); + blobmsg_close_table(&b, c); + + c = blobmsg_open_table(&b, "dns_action"); + interface_dump_action(&b, iface, cl->data.dns_class); + blobmsg_close_table(&b, c); + + blobmsg_add_u64(&b, "bytes_ul", cl->data.bytes_ul); + blobmsg_add_u64(&b, "bytes_dl", cl->data.bytes_dl); +} + +static int +client_ubus_get(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__CLIENT_ATTR_MAX]; + struct interface *iface = NULL; + const void *addr = NULL; + const char *id = NULL; + struct client *cl = NULL; + int ret; + + ret = client_ubus_init(msg, tb, &iface, &addr, &id, &cl); + if (ret) + return ret; + + if (!cl) + return UBUS_STATUS_NOT_FOUND; + + blob_buf_init(&b, 0); + blobmsg_add_string(&b, "address", ether_ntoa(cl->node.key)); + client_dump(iface, cl); + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + +static int +client_ubus_list(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *iface_attr; + struct interface *iface; + struct client *cl; + + blobmsg_parse(&client_policy[CLIENT_ATTR_IFACE], 1, &iface_attr, + blobmsg_data(msg), blobmsg_len(msg)); + + if (!iface_attr) + return UBUS_STATUS_INVALID_ARGUMENT; + + iface = avl_find_element(&interfaces, blobmsg_get_string(iface_attr), + iface, node); + if (!iface) + return UBUS_STATUS_NOT_FOUND; + + blob_buf_init(&b, 0); + avl_for_each_element(&iface->clients, cl, node) { + void *c; + + c = blobmsg_open_table(&b, ether_ntoa(cl->node.key)); + client_dump(iface, cl); + blobmsg_close_table(&b, c); + } + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + +enum { + WHITELIST_ATTR_IFACE, + WHITELIST_ATTR_ADDR, + WHITELIST_ATTR_STATE, + __WHITELIST_ATTR_MAX +}; + +static const struct blobmsg_policy whitelist_policy[__WHITELIST_ATTR_MAX] = { + [WHITELIST_ATTR_IFACE] = { "interface", BLOBMSG_TYPE_STRING }, + [WHITELIST_ATTR_ADDR] = { "address", BLOBMSG_TYPE_ARRAY }, + [WHITELIST_ATTR_STATE] = { "state", BLOBMSG_TYPE_INT32 }, +}; + +static int +whitelist_update(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__WHITELIST_ATTR_MAX]; + struct interface *iface; + struct blob_attr *cur; + uint8_t state = 0; + const uint8_t *val = &state; + int rem; + + blobmsg_parse(whitelist_policy, __WHITELIST_ATTR_MAX, tb, + blobmsg_data(msg), blobmsg_len(msg)); + + if ((cur = tb[WHITELIST_ATTR_IFACE]) != NULL) + iface = avl_find_element(&interfaces, blobmsg_get_string(cur), + iface, node); + else + return UBUS_STATUS_INVALID_ARGUMENT; + + if ((cur = tb[WHITELIST_ATTR_STATE]) != NULL) + state = blobmsg_get_u32(cur); + + if ((cur = tb[WHITELIST_ATTR_ADDR]) == NULL || + blobmsg_check_array(cur, BLOBMSG_TYPE_STRING) < 0) + return UBUS_STATUS_INVALID_ARGUMENT; + + if (!strcmp(method, "whitelist_remove")) + val = NULL; + + blobmsg_for_each_attr(cur, tb[WHITELIST_ATTR_ADDR], rem) { + const char *addrstr = blobmsg_get_string(cur); + bool ipv6 = strchr(addrstr, ':'); + union { + struct in_addr in; + struct in6_addr in6; + } addr = {}; + + if (inet_pton(ipv6 ? AF_INET6 : AF_INET, addrstr, &addr) != 1) + continue; + + spotfilter_bpf_set_whitelist(iface, &addr, ipv6, val); + } + + return 0; +} + +static const struct ubus_method spotfilter_methods[] = { + UBUS_METHOD_NOARG("check_devices", check_devices), + UBUS_METHOD("client_set", client_ubus_update, client_policy), + UBUS_METHOD_MASK("client_remove", client_ubus_update, client_policy, + (1 << CLIENT_ATTR_IFACE) | (1 << CLIENT_ATTR_ADDR)), + UBUS_METHOD_MASK("client_get", client_ubus_get, client_policy, + (1 << CLIENT_ATTR_IFACE) | (1 << CLIENT_ATTR_ADDR)), + UBUS_METHOD_MASK("client_list", client_ubus_list, client_policy, + (1 << CLIENT_ATTR_IFACE)), + UBUS_METHOD("interface_add", interface_ubus_add, iface_policy), + UBUS_METHOD_MASK("interface_remove", interface_ubus_remove, + iface_policy, 1 << IFACE_ATTR_NAME), + UBUS_METHOD("whitelist_add", whitelist_update, whitelist_policy), + UBUS_METHOD_MASK("whitelist_remove", whitelist_update, whitelist_policy, + (1 << WHITELIST_ATTR_IFACE) | (1 << WHITELIST_ATTR_ADDR)), +}; + +static struct ubus_object_type spotfilter_object_type = + UBUS_OBJECT_TYPE("spotfilter", spotfilter_methods); + +static struct ubus_object spotfilter_object = { + .name = "spotfilter", + .type = &spotfilter_object_type, + .methods = spotfilter_methods, + .n_methods = ARRAY_SIZE(spotfilter_methods), +}; + +static void +ubus_connect_handler(struct ubus_context *ctx) +{ + ubus_add_object(ctx, &spotfilter_object); +} + +static struct ubus_auto_conn conn; + +void spotfilter_ubus_notify(struct interface *iface, struct client *cl, const char *type) +{ + blob_buf_init(&b, 0); + blobmsg_add_string(&b, "interface", interface_name(iface)); + if (cl) { + blobmsg_add_string(&b, "address", ether_ntoa(cl->node.key)); + if (cl->id_node.key) + blobmsg_add_string(&b, "id", cl->id_node.key); + } + + ubus_notify(&conn.ctx, &spotfilter_object, type, b.head, -1); +} + +int spotfilter_ubus_init(void) +{ + conn.cb = ubus_connect_handler; + ubus_auto_connect(&conn); + + return 0; +} + +void spotfilter_ubus_stop(void) +{ + ubus_auto_shutdown(&conn); +}