spotfilter: add advance captive packet filter

Signed-off-by: John Crispin <john@phrozen.org>
This commit is contained in:
John Crispin
2022-08-17 14:52:06 +02:00
parent baaa31f445
commit d69a8f159d
22 changed files with 3359 additions and 0 deletions

View File

@@ -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 <nbd@nbd.name>
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))

View File

@@ -0,0 +1,2 @@
#!/bin/sh
ubus call spotfilter check_devices

View File

@@ -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
}

View File

@@ -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}
)

View File

@@ -0,0 +1,196 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <sys/resource.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <glob.h>
#include <unistd.h>
#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;
}

View File

@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#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

View File

@@ -0,0 +1,173 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#ifndef __BPF_SKB_UTILS_H
#define __BPF_SKB_UTILS_H
#include <uapi/linux/bpf.h>
#include <uapi/linux/if_ether.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/ipv6.h>
#include <linux/ip.h>
#include <net/ipv6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
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

View File

@@ -0,0 +1,203 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <netinet/if_ether.h>
#include <libubox/uloop.h>
#include <libubox/avl-cmp.h>
#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);
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#ifndef __SPOTFILTER_CLIENT_H
#define __SPOTFILTER_CLIENT_H
#include <netinet/if_ether.h>
#include <libubox/kvlist.h>
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

View File

@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#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);
}

View File

@@ -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" ]
}
]
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <netinet/icmp6.h>
#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);
}

View File

@@ -0,0 +1,369 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/ether.h>
#include <net/if.h>
#include <libubox/avl-cmp.h>
#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);
}

View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#ifndef __SPOTFILTER_INTERFACE_H
#define __SPOTFILTER_INTERFACE_H
#include <libubox/vlist.h>
#include <libubox/blobmsg.h>
#include <libubox/uloop.h>
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

View File

@@ -0,0 +1,120 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <libubox/uloop.h>
#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;
}

View File

@@ -0,0 +1,276 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#define _GNU_SOURCE
#include <linux/netlink.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/family.h>
#include <netlink/genl/ctrl.h>
#include <netlink/msg.h>
#include <netlink/attr.h>
#include <linux/nl80211.h>
#include <errno.h>
#include <libubox/uloop.h>
#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;
}

View File

@@ -0,0 +1,99 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/socket.h>
#include <netlink/msg.h>
#include <netlink/attr.h>
#include <netlink/socket.h>
#include <linux/rtnetlink.h>
#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;
}

View File

@@ -0,0 +1,626 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <netinet/if_ether.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/udp.h>
#include <netpacket/packet.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <resolv.h>
#include <libubox/uloop.h>
#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);
}

View File

@@ -0,0 +1,287 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#define KBUILD_MODNAME "foo"
#include <uapi/linux/bpf.h>
#include <uapi/linux/if_ether.h>
#include <uapi/linux/if_packet.h>
#include <uapi/linux/ip.h>
#include <uapi/linux/ipv6.h>
#include <uapi/linux/in.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/udp.h>
#include <uapi/linux/icmpv6.h>
#include <uapi/linux/filter.h>
#include <uapi/linux/pkt_cls.h>
#include <linux/ip.h>
#include <net/ipv6.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#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";

View File

@@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#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

View File

@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#ifndef __SPOTFILTER_H
#define __SPOTFILTER_H
#include <stdbool.h>
#include <stdint.h>
#include <regex.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <libubox/utils.h>
#include <libubox/avl.h>
#include <libubox/vlist.h>
#include <libubox/blobmsg.h>
#include <libubox/ulog.h>
#include <netinet/in.h>
#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

View File

@@ -0,0 +1,470 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
*/
#include <libubus.h>
#include <arpa/inet.h>
#include <netinet/ether.h>
#include <net/if.h>
#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);
}