diff --git a/rpcd-mod-luci/Makefile b/rpcd-mod-luci/Makefile new file mode 100644 index 000000000..c5ed10196 --- /dev/null +++ b/rpcd-mod-luci/Makefile @@ -0,0 +1,55 @@ +# +# Copyright (C) 2016-2017 Jo-Philipp Wich +# +# Licensed under the Apache License, Version 2.0. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=rpcd-mod-luci +PKG_VERSION:=20240305 +PKG_RELEASE:=1 +PKG_MAINTAINER:=Jo-Philipp Wich + +PKG_LICENSE:=Apache-2.0 + +PKG_BUILD_PARALLEL:=1 +PKG_BUILD_DEPENDS:=iwinfo + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +TARGET_CFLAGS += \ + -I$(STAGING_DIR)/usr/include/libnl-tiny \ + -I$(STAGING_DIR)/usr/include \ + +CMAKE_OPTIONS += \ + -DLIBNL_LIBS=-lnl-tiny \ + +define Build/Prepare + $(INSTALL_DIR) $(PKG_BUILD_DIR) + $(CP) ./src/* $(PKG_BUILD_DIR)/ +endef + +define Package/rpcd-mod-luci + SECTION:=libs + CATEGORY:=Libraries + TITLE:=Rapid reverse DNS rpcd module + DEPENDS:=+rpcd +libubox +libubus +libnl-tiny +endef + +define Package/rpcd-mod-luci/description + Provides LuCI backend ubus RPC operations. +endef + +define Package/rpcd-mod-luci/install + $(INSTALL_DIR) $(1)/usr/lib/rpcd + $(INSTALL_BIN) $(PKG_BUILD_DIR)/luci.so $(1)/usr/lib/rpcd/ +endef + +define Package/rpcd-mod-luci/postinst +#!/bin/sh +[ -n "$$IPKG_INSTROOT" ] || /etc/init.d/rpcd reload +endef + +$(eval $(call BuildPackage,rpcd-mod-luci)) diff --git a/rpcd-mod-luci/src/CMakeLists.txt b/rpcd-mod-luci/src/CMakeLists.txt new file mode 100644 index 000000000..24d60ac70 --- /dev/null +++ b/rpcd-mod-luci/src/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.31) + +PROJECT(rpcd-mod-luci C) + +ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations) + +SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") + +IF(APPLE) + INCLUDE_DIRECTORIES(/opt/local/include) + LINK_DIRECTORIES(/opt/local/lib) +ENDIF() + +IF (NOT DEFINED LIBNL_LIBS) + FIND_LIBRARY(libnl NAMES libnl-3 libnl nl-3 nl) + SET(LIBNL_LIBS ${libnl}) +ENDIF() + +ADD_LIBRARY(rpcd-mod-luci MODULE luci.c traffic.c webprobe.c) +TARGET_LINK_LIBRARIES(rpcd-mod-luci ubox ubus uci dl ${LIBNL_LIBS}) +SET_TARGET_PROPERTIES(rpcd-mod-luci PROPERTIES OUTPUT_NAME luci PREFIX "") + +INSTALL(TARGETS rpcd-mod-luci LIBRARY DESTINATION lib) diff --git a/rpcd-mod-luci/src/luci.c b/rpcd-mod-luci/src/luci.c new file mode 100644 index 000000000..b59757977 --- /dev/null +++ b/rpcd-mod-luci/src/luci.c @@ -0,0 +1,2113 @@ +/* + * luci - LuCI core functions plugin for rpcd + * + * Copyright (C) 2019 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#ifndef IN6_IS_ADDR_ULA +#define IN6_IS_ADDR_ULA(a) (((a)->s6_addr[0] & 0xfe) == 0xfc) +#endif + + +int rpc_luci_traffic_init(struct ubus_context *ctx); +int rpc_luci_webprobe_init(struct ubus_context *ctx); +void rpc_luci_dhcp4_clients(void (*add)(const struct in_addr *, const unsigned char *)); +void rpc_luci_traffic_discover(void (*add)(int, const void *, const unsigned char *)); + +static struct blob_buf blob; + +struct reply_context { + struct ubus_context *context; + struct ubus_request_data request; + struct uloop_timeout timeout; + struct blob_buf blob; + struct avl_tree avl; + int pending; +}; + +struct invoke_context { + struct ubus_request request; + struct uloop_timeout timeout; + struct ubus_context *context; + void (*cb)(struct ubus_request *, int, struct blob_attr *); + void *priv; +}; + +typedef const char * const iw_text_t; +static iw_text_t *iw_modenames, *iw_authnames, *iw_kmgmtnames, + *iw_ciphernames, *iw_htmodenames, *iw_80211names; +static struct iwinfo_ops *(*iw_backend)(const char *); +static void (*iw_close)(void); +static size_t (*iw_format_hwmodes)(int, char *, size_t); + +static void +invoke_data_cb(struct ubus_request *req, int type, struct blob_attr *msg) +{ + struct invoke_context *ictx = + container_of(req, struct invoke_context, request); + + if (ictx->cb != NULL) + ictx->cb(req, type, msg); + + ictx->cb = NULL; +} + +static void +invoke_done_cb(struct ubus_request *req, int ret) +{ + struct invoke_context *ictx = + container_of(req, struct invoke_context, request); + + if (ictx->cb != NULL) + ictx->cb(req, -1, NULL); + + uloop_timeout_cancel(&ictx->timeout); + free(ictx); +} + +static void +invoke_timeout_cb(struct uloop_timeout *timeout) +{ + struct invoke_context *ictx = + container_of(timeout, struct invoke_context, timeout); + + if (ictx->cb != NULL) + ictx->cb(&ictx->request, -1, NULL); + + ubus_abort_request(ictx->context, &ictx->request); + free(ictx); +} + +static struct reply_context * +defer_request(struct ubus_context *ctx, struct ubus_request_data *req) +{ + struct reply_context *rctx; + + rctx = calloc(1, sizeof(*rctx)); + + if (!rctx) + return NULL; + + rctx->context = ctx; + blob_buf_init(&rctx->blob, 0); + ubus_defer_request(ctx, req, &rctx->request); + + return rctx; +} + +static int +finish_request(struct reply_context *rctx, int status) +{ + if (status == UBUS_STATUS_OK) + ubus_send_reply(rctx->context, &rctx->request, rctx->blob.head); + + ubus_complete_deferred_request(rctx->context, &rctx->request, status); + blob_buf_free(&rctx->blob); + free(rctx); + + return status; +} + +static bool +invoke_ubus(struct ubus_context *ctx, const char *object, const char *method, + struct blob_buf *req, + void (*cb)(struct ubus_request *, int, struct blob_attr *), + void *priv) +{ + struct invoke_context *ictx; + struct blob_buf empty = {}; + uint32_t id; + bool rv; + + if (ubus_lookup_id(ctx, object, &id)) + return false; + + if (req == NULL) { + blob_buf_init(&empty, 0); + req = ∅ + } + + ictx = calloc(1, sizeof(*ictx)); + + if (ictx == NULL) + return false; + + ictx->context = ctx; + rv = !ubus_invoke_async(ctx, id, method, req->head, &ictx->request); + + if (rv) { + ictx->cb = cb; + ictx->request.priv = priv; + ictx->request.data_cb = invoke_data_cb; + ictx->request.complete_cb = invoke_done_cb; + ubus_complete_request_async(ctx, &ictx->request); + + ictx->timeout.cb = invoke_timeout_cb; + uloop_timeout_set(&ictx->timeout, 2000); + } + else { + if (cb != NULL) + cb(&ictx->request, -1, NULL); + + free(ictx); + } + + if (req == &empty) + blob_buf_free(req); + + return rv; +} + +static char * +readstr(const char *fmt, ...) +{ + static char data[128]; + char path[128]; + va_list ap; + size_t n; + FILE *f; + + va_start(ap, fmt); + vsnprintf(path, sizeof(path), fmt, ap); + va_end(ap); + + data[0] = 0; + f = fopen(path, "r"); + + if (f != NULL) { + n = fread(data, 1, sizeof(data) - 1, f); + data[n] = 0; + + while (n > 0 && isspace(data[n-1])) + data[--n] = 0; + + fclose(f); + } + + return data; +} + +static bool +ea_empty(struct ether_addr *ea) +{ + struct ether_addr empty = { 0 }; + + return !memcmp(ea, &empty, sizeof(empty)); +} + +static char * +ea2str(struct ether_addr *ea) +{ + static char mac[18]; + + if (!ea) + return NULL; + + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + ea->ether_addr_octet[0], ea->ether_addr_octet[1], + ea->ether_addr_octet[2], ea->ether_addr_octet[3], + ea->ether_addr_octet[4], ea->ether_addr_octet[5]); + + return mac; +} + +static char * +sa2str(struct sockaddr *sa) +{ + static char buf[INET6_ADDRSTRLEN]; + union { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + struct sockaddr_ll *ll; + struct sockaddr *sa; + } s; + + s.sa = sa; + + if (s.sa->sa_family == AF_INET) + inet_ntop(sa->sa_family, &s.in->sin_addr, buf, sizeof(buf)); + else if (s.sa->sa_family == AF_INET6) + inet_ntop(sa->sa_family, &s.in6->sin6_addr, buf, sizeof(buf)); + else if (s.sa->sa_family == AF_PACKET) + strcpy(buf, ea2str((struct ether_addr *)s.ll->sll_addr)); + else + buf[0] = 0; + + return buf; +} + +static struct ether_addr * +duid2ea(const char *duid) +{ + static struct ether_addr ea; + const char *p = NULL; + size_t len; + + if (!duid) + return NULL; + + for (len = 0; duid[len]; len++) + if (!isxdigit(duid[len])) + return NULL; + +#define hex(x) \ + (((x) <= '9') ? ((x) - '0') : \ + (((x) <= 'F') ? ((x) - 'A' + 10) : \ + ((x) - 'a' + 10))) + + switch (len) { + case 28: + if (!strncmp(duid, "00010001", 8)) + p = duid + 16; + + break; + + case 20: + if (!strncmp(duid, "00030001", 8)) + p = duid + 8; + + break; + + case 12: + p = duid; + break; + } + + if (!p) + return NULL; + + ea.ether_addr_octet[0] = hex(p[0]) * 16 + hex(p[1]); + ea.ether_addr_octet[1] = hex(p[2]) * 16 + hex(p[3]); + ea.ether_addr_octet[2] = hex(p[4]) * 16 + hex(p[5]); + ea.ether_addr_octet[3] = hex(p[6]) * 16 + hex(p[7]); + ea.ether_addr_octet[4] = hex(p[8]) * 16 + hex(p[9]); + ea.ether_addr_octet[5] = hex(p[10]) * 16 + hex(p[11]); + + return &ea; +} + +static void strip_colon(char *str) { + char *pr = str, *pw = str; + + while (*pr) { + *pw = *pr++; + pw += (*pw != ':'); + } + *pw = '\0'; +} + +static struct { + time_t now; + size_t num, off; + struct { + FILE *fh; + bool odhcpd; + } *files; +} lease_state = { }; + +struct lease_entry { + int af, n_addr; + char buf[512]; + int32_t expire; + struct ether_addr mac; + char *iface; + char *hostname; + char *duid; + char *iaid; + union { + struct in_addr in; + struct in6_addr in6; + } addr[10]; + uint8_t mask; +}; + +static bool +add_leasefile(const char *path, bool is_odhcpd) +{ + void *ptr; + FILE *fh; + + fh = fopen(path, "r"); + + if (!fh) + return false; + + ptr = realloc(lease_state.files, sizeof(*lease_state.files) * (lease_state.num + 1)); + + if (!ptr) { + fclose(fh); + + return false; + } + + lease_state.files = ptr; + lease_state.files[lease_state.num].fh = fh; + lease_state.files[lease_state.num].odhcpd = is_odhcpd; + lease_state.num++; + + return true; +} + +static bool +find_leasefiles(struct uci_context *uci, bool is_odhcpd) +{ + struct uci_ptr ptr = { .package = "dhcp" }; + struct uci_package *pkg = NULL; + struct uci_section *s; + struct uci_element *e; + bool found = false; + + pkg = uci_lookup_package(uci, ptr.package); + + if (!pkg) { + uci_load(uci, ptr.package, &pkg); + + if (!pkg) + return NULL; + } + + uci_foreach_element(&pkg->sections, e) { + s = uci_to_section(e); + + if (strcmp(s->type, is_odhcpd ? "odhcpd" : "dnsmasq")) + continue; + + ptr.flags = 0; + + ptr.section = s->e.name; + ptr.s = NULL; + + ptr.option = "leasefile"; + ptr.o = NULL; + + if (uci_lookup_ptr(uci, &ptr, NULL, true) || ptr.o == NULL) + continue; + + if (ptr.o->type != UCI_TYPE_STRING) + continue; + + if (add_leasefile(ptr.o->v.string, is_odhcpd)) + found = true; + } + + return found; +} + +static void +lease_close(void) +{ + while (lease_state.num > 0) + fclose(lease_state.files[--lease_state.num].fh); + + free(lease_state.files); + + lease_state.files = NULL; + lease_state.num = 0; + lease_state.off = 0; +} + +static void +lease_open(void) +{ + struct uci_context *uci; + + lease_close(); + + uci = uci_alloc_context(); + + if (!uci) + return; + + lease_state.now = time(NULL); + + if (!find_leasefiles(uci, false)) + add_leasefile("/tmp/dhcp.leases", false); + + if (!find_leasefiles(uci, true)) + add_leasefile("/tmp/odhcpd.leases", true); + + uci_free_context(uci); +} + +static struct lease_entry * +lease_next(void) +{ + static struct lease_entry e; + struct ether_addr *ea; + char *p; + int n; + + while (lease_state.off < lease_state.num) { + memset(&e, 0, sizeof(e)); + + while (fgets(e.buf, sizeof(e.buf), lease_state.files[lease_state.off].fh)) { + ea = NULL; + + if (lease_state.files[lease_state.off].odhcpd) { + p = strtok(e.buf, " \t\n"); /* # */ + if (!p || strcmp(p, "#")) + continue; + + e.iface = strtok(NULL, " \t\n"); /* iface */ + if (!e.iface) + continue; + + e.duid = strtok(NULL, " \t\n"); /* duid or MAC */ + if (!e.duid) + continue; + + e.iaid = strtok(NULL, " \t\n"); /* iaid or "ipv4"*/ + if (!e.iaid) + continue; + + if (!strcmp(e.iaid, "ipv4")) { + e.af = AF_INET; + e.mask = 32; + ea = ether_aton(e.duid); + e.duid = NULL; + e.iaid = NULL; + } else { + e.af = AF_INET6; + e.mask = 128; + } + + e.hostname = strtok(NULL, " \t\n"); /* name */ + if (!e.hostname) + continue; + + p = strtok(NULL, " \t\n"); /* ts */ + if (!p) + continue; + + n = strtol(p, NULL, 10); + if (n > lease_state.now) + e.expire = n - lease_state.now; + else if (n >= 0) + e.expire = 0; + else + e.expire = -1; + + strtok(NULL, " \t\n"); /* id */ + + p = strtok(NULL, " \t\n"); /* length */ + if (!p) + continue; + + n = atoi(p); /* length */ + if (n != 0) + e.mask = n; + + for (e.n_addr = 0, p = strtok(NULL, "/ \t\n"); + e.n_addr < ARRAY_SIZE(e.addr) && p != NULL; + p = strtok(NULL, "/ \t\n")) { + if (inet_pton(e.af, p, &e.addr[e.n_addr].in6)) + e.n_addr++; + } + + if (!ea) + ea = duid2ea(e.duid); + + if (ea) + e.mac = *ea; + + if (!strcmp(e.hostname, "-")) + e.hostname = NULL; + + if (e.duid && !strcmp(e.duid, "-")) + e.duid = NULL; + } else { + p = strtok(e.buf, " \t\n"); + + if (!p) + continue; + + n = strtol(p, NULL, 10); + + if (n > lease_state.now) + e.expire = n - lease_state.now; + else if (n > 0) + e.expire = 0; + else + e.expire = -1; + + p = strtok(NULL, " \t\n"); + + if (!p) + continue; + + ea = ether_aton(p); + + p = strtok(NULL, " \t\n"); + + if (p && inet_pton(AF_INET6, p, &e.addr[0].in6)) { + e.af = AF_INET6; + e.mask = 128; + e.n_addr = 1; + } + else if (p && inet_pton(AF_INET, p, &e.addr[0].in)) { + e.af = AF_INET; + e.mask = 32; + e.n_addr = 1; + } + else { + continue; + } + + if (!ea && e.af != AF_INET6) + continue; + + e.hostname = strtok(NULL, " \t\n"); + e.duid = strtok(NULL, " \t\n"); + e.iaid = NULL; + + if (!e.hostname || !e.duid) + continue; + + if (!strcmp(e.hostname, "*")) + e.hostname = NULL; + + if (e.af == AF_INET && strlen(e.duid) > 15 && !strncmp(e.duid, "ff:", 3)) { + /* ff:: */ + e.duid[14] = '\0'; + e.iaid = &e.duid[3]; + strip_colon(e.iaid); + e.duid = &e.duid[15]; + strip_colon(e.duid); + } else if(e.af == AF_INET && strlen(e.duid) == 20 && !strncmp(e.duid, "01:", 3)) { + /* 01: */ + if (!ea) + ea = ether_aton(&e.duid[3]); + e.duid = NULL; + } else if (!strcmp(e.duid, "*")) { + e.duid = NULL; + } else { + strip_colon(e.duid); + } + + if (!ea && e.duid) + ea = duid2ea(e.duid); + + if (ea) + e.mac = *ea; + } + + return &e; + } + + lease_state.off++; + } + + return NULL; +} + + +static void +rpc_luci_parse_network_device_sys(const char *name, struct ifaddrs *ifaddr) +{ + char link[64], buf[512], *p; + unsigned int ifa_flags = 0; + struct sockaddr_ll *sll; + struct ifaddrs *ifa; + struct dirent *e; + void *o, *o2, *a; + ssize_t len; + uint64_t v; + int n, af; + DIR *d; + + const char *stats[] = { + "rx_bytes", "tx_bytes", "tx_errors", "rx_errors", "tx_packets", + "rx_packets", "multicast", "collisions", "rx_dropped", "tx_dropped" + }; + + o = blobmsg_open_table(&blob, name); + + blobmsg_add_string(&blob, "name", name); + + snprintf(buf, sizeof(buf), "/sys/class/net/%s/brif", name); + + d = opendir(buf); + + if (d) { + blobmsg_add_u8(&blob, "bridge", 1); + + a = blobmsg_open_array(&blob, "ports"); + + while (true) { + e = readdir(d); + + if (e == NULL) + break; + + if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) + blobmsg_add_string(&blob, NULL, e->d_name); + } + + blobmsg_close_array(&blob, a); + + closedir(d); + + p = readstr("/sys/class/net/%s/bridge/bridge_id", name); + blobmsg_add_string(&blob, "id", p); + + p = readstr("/sys/class/net/%s/bridge/stp_state", name); + blobmsg_add_u8(&blob, "stp", strcmp(p, "0") ? 1 : 0); + } + + snprintf(buf, sizeof(buf), "/sys/class/net/%s/master", name); + len = readlink(buf, link, sizeof(link) - 1); + + if (len > 0) { + link[len] = 0; + blobmsg_add_string(&blob, "master", basename(link)); + } + + p = readstr("/sys/class/net/%s/phy80211/index", name); + blobmsg_add_u8(&blob, "wireless", *p ? 1 : 0); + + p = readstr("/sys/class/net/%s/operstate", name); + blobmsg_add_u8(&blob, "up", !strcmp(p, "up") || !strcmp(p, "unknown")); + + n = atoi(readstr("/sys/class/net/%s/mtu", name)); + if (n > 0) + blobmsg_add_u32(&blob, "mtu", n); + + n = atoi(readstr("/sys/class/net/%s/tx_queue_len", name)); + if (n > 0) + blobmsg_add_u32(&blob, "qlen", n); + + p = readstr("/sys/class/net/%s/master", name); + if (*p) + blobmsg_add_string(&blob, "master", p); + + p = strstr(readstr("/sys/class/net/%s/uevent", name), "DEVTYPE="); + if (p) { + for (n = 0, p += strlen("DEVTYPE=");; n++) { + if (p[n] == '\0' || p[n] == '\n') { + p[n] = 0; + blobmsg_add_string(&blob, "devtype", p); + break; + } + } + } + else { + blobmsg_add_string(&blob, "devtype", "ethernet"); + } + + for (af = AF_INET; af != 0; af = (af == AF_INET) ? AF_INET6 : 0) { + a = blobmsg_open_array(&blob, + (af == AF_INET) ? "ipaddrs" : "ip6addrs"); + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != af) + continue; + + if (strcmp(ifa->ifa_name, name)) + continue; + + o2 = blobmsg_open_table(&blob, NULL); + + blobmsg_add_string(&blob, "address", + sa2str(ifa->ifa_addr)); + + blobmsg_add_string(&blob, "netmask", + sa2str(ifa->ifa_netmask)); + + if (ifa->ifa_dstaddr && (ifa->ifa_flags & IFF_POINTOPOINT)) + blobmsg_add_string(&blob, "remote", + sa2str(ifa->ifa_dstaddr)); + else if (ifa->ifa_broadaddr && (ifa->ifa_flags & IFF_BROADCAST)) + blobmsg_add_string(&blob, "broadcast", + sa2str(ifa->ifa_broadaddr)); + + blobmsg_close_table(&blob, o2); + + ifa_flags |= ifa->ifa_flags; + } + + blobmsg_close_array(&blob, a); + } + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_PACKET) + continue; + + if (strcmp(ifa->ifa_name, name)) + continue; + + sll = (struct sockaddr_ll *)ifa->ifa_addr; + + if (sll->sll_hatype == 1) + blobmsg_add_string(&blob, "mac", sa2str(ifa->ifa_addr)); + + blobmsg_add_u32(&blob, "type", sll->sll_hatype); + blobmsg_add_u32(&blob, "ifindex", sll->sll_ifindex); + + ifa_flags |= ifa->ifa_flags; + + n = atoi(readstr("/sys/class/net/%s/iflink", name)); + + if (n != sll->sll_ifindex) { + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_PACKET) + continue; + + sll = (struct sockaddr_ll *)ifa->ifa_addr; + + if (sll->sll_ifindex != n) + continue; + + blobmsg_add_string(&blob, "parent", ifa->ifa_name); + break; + } + } + + break; + } + + o2 = blobmsg_open_table(&blob, "stats"); + + for (n = 0; n < ARRAY_SIZE(stats); n++) { + v = strtoull(readstr("/sys/class/net/%s/statistics/%s", + name, stats[n]), NULL, 10); + + blobmsg_add_u64(&blob, stats[n], v); + } + + blobmsg_close_table(&blob, o2); + + o2 = blobmsg_open_table(&blob, "flags"); + blobmsg_add_u8(&blob, "up", !!(ifa_flags & IFF_UP)); + blobmsg_add_u8(&blob, "broadcast", !!(ifa_flags & IFF_BROADCAST)); + blobmsg_add_u8(&blob, "promisc", !!(ifa_flags & IFF_PROMISC)); + blobmsg_add_u8(&blob, "loopback", !!(ifa_flags & IFF_LOOPBACK)); + blobmsg_add_u8(&blob, "noarp", !!(ifa_flags & IFF_NOARP)); + blobmsg_add_u8(&blob, "multicast", !!(ifa_flags & IFF_MULTICAST)); + blobmsg_add_u8(&blob, "pointtopoint", !!(ifa_flags & IFF_POINTOPOINT)); + blobmsg_close_table(&blob, o2); + + o2 = blobmsg_open_table(&blob, "link"); + + p = readstr("/sys/class/net/%s/speed", name); + if (*p) + blobmsg_add_u32(&blob, "speed", atoi(p)); + + p = readstr("/sys/class/net/%s/duplex", name); + if (*p) + blobmsg_add_string(&blob, "duplex", p); + + n = atoi(readstr("/sys/class/net/%s/carrier", name)); + blobmsg_add_u8(&blob, "carrier", n == 1); + + n = atoi(readstr("/sys/class/net/%s/carrier_changes", name)); + blobmsg_add_u32(&blob, "changes", n); + + n = atoi(readstr("/sys/class/net/%s/carrier_up_count", name)); + blobmsg_add_u32(&blob, "up_count", n); + + n = atoi(readstr("/sys/class/net/%s/carrier_down_count", name)); + blobmsg_add_u32(&blob, "down_count", n); + + blobmsg_close_table(&blob, o2); + + blobmsg_close_table(&blob, o); +} + +static int +rpc_luci_get_network_devices(struct ubus_context *ctx, + struct ubus_object *obj, + struct ubus_request_data *req, + const char *method, + struct blob_attr *msg) +{ + struct ifaddrs *ifaddr; + struct dirent *e; + DIR *d; + + blob_buf_init(&blob, 0); + + d = opendir("/sys/class/net"); + + if (d != NULL) { + if (getifaddrs(&ifaddr) == 1) + ifaddr = NULL; + + while (true) { + e = readdir(d); + + if (e == NULL) + break; + + if (e->d_type != DT_DIR && e->d_type != DT_REG) + rpc_luci_parse_network_device_sys(e->d_name, ifaddr); + } + + if (ifaddr != NULL) + freeifaddrs(ifaddr); + + closedir(d); + } + + ubus_send_reply(ctx, req, blob.head); + return 0; +} + + +static void +iw_call_str(int (*method)(const char *, char *), const char *dev, + struct blob_buf *blob, const char *field) +{ + char buf[IWINFO_BUFSIZE] = {}; + + if (method(dev, buf) == 0) + blobmsg_add_string(blob, field, buf); +} + +static void +iw_call_num(int (*method)(const char *, int *), const char *dev, + struct blob_buf *blob, const char *field) +{ + int val = 0; + + if (method(dev, &val) == 0) + blobmsg_add_u32(blob, field, val); +} + +static void +iw_lower(const char *src, char *dst, size_t len) +{ + size_t i; + + for (i = 0; *src && i < len; i++) + *dst++ = tolower(*src++); + + *dst = 0; +} + +static void +iw_add_bit_array(struct blob_buf *buf, const char *name, uint32_t bits, + iw_text_t values[], size_t len, bool lower, uint32_t zero) +{ + void *c; + size_t i; + char l[128]; + const char *v; + + if (!bits) + bits = zero; + + c = blobmsg_open_array(buf, name); + + for (i = 0; i < len; i++) + if (bits & 1 << i) + { + v = values[i]; + + if (lower) + { + iw_lower(v, l, strlen(values[i])); + v = l; + } + + blobmsg_add_string(buf, NULL, v); + } + + blobmsg_close_array(buf, c); +} + +static bool rpc_luci_get_iwinfo(struct blob_buf *buf, const char *devname, + bool phy_only) +{ + struct iwinfo_crypto_entry crypto = {}; + struct iwinfo_hardware_id ids = {}; + const struct iwinfo_ops *iw; + void *iwlib = NULL; + void *o, *o2, *a; + glob_t paths; + int nret, i; + char text[32]; + + if (!iw_backend || !iw_close || !iw_format_hwmodes || !iw_modenames || !iw_80211names || + !iw_htmodenames || !iw_authnames || !iw_kmgmtnames || !iw_ciphernames) { + if (glob("/usr/lib/libiwinfo.so*", 0, NULL, &paths) != 0) + return false; + + for (i = 0; i < paths.gl_pathc && !iwlib; i++) + iwlib = dlopen(paths.gl_pathv[i], RTLD_LAZY | RTLD_LOCAL); + + globfree(&paths); + + if (!iwlib) + return false; + + iw_backend = dlsym(iwlib, "iwinfo_backend"); + iw_close = dlsym(iwlib, "iwinfo_close"); + iw_format_hwmodes = dlsym(iwlib, "iwinfo_format_hwmodes"); + iw_modenames = dlsym(iwlib, "IWINFO_OPMODE_NAMES"); + iw_80211names = dlsym(iwlib, "IWINFO_80211_NAMES"); + iw_htmodenames = dlsym(iwlib, "IWINFO_HTMODE_NAMES"); + iw_authnames = dlsym(iwlib, "IWINFO_AUTH_NAMES"); + iw_kmgmtnames = dlsym(iwlib, "IWINFO_KMGMT_NAMES"); + iw_ciphernames = dlsym(iwlib, "IWINFO_CIPHER_NAMES"); + + if (!iw_backend || !iw_close || !iw_format_hwmodes || !iw_modenames || !iw_80211names || + !iw_htmodenames || !iw_authnames || !iw_kmgmtnames || !iw_ciphernames) + return false; + } + + iw = iw_backend(devname); + + if (!iw) + return false; + + o = blobmsg_open_table(buf, "iwinfo"); + + iw_call_num(iw->signal, devname, buf, "signal"); + iw_call_num(iw->noise, devname, buf, "noise"); + iw_call_num(iw->channel, devname, buf, "channel"); + iw_call_str(iw->country, devname, buf, "country"); + iw_call_str(iw->phyname, devname, buf, "phy"); + iw_call_num(iw->txpower, devname, buf, "txpower"); + iw_call_num(iw->txpower_offset, devname, buf, "txpower_offset"); + iw_call_num(iw->frequency, devname, buf, "frequency"); + iw_call_num(iw->frequency_offset, devname, buf, "frequency_offset"); + + if (!iw->hwmodelist(devname, &nret)) { + iw_add_bit_array(buf, "hwmodes", nret, + iw_80211names, IWINFO_80211_COUNT, true, 0); + + if (iw_format_hwmodes(nret, text, sizeof(text)) > 0) + blobmsg_add_string(buf, "hwmodes_text", text); + } + + if (!iw->htmodelist(devname, &nret)) + iw_add_bit_array(buf, "htmodes", nret & ~IWINFO_HTMODE_NOHT, + iw_htmodenames, IWINFO_HTMODE_COUNT, false, 0); + + if (!iw->hardware_id(devname, (char *)&ids)) { + o2 = blobmsg_open_table(buf, "hardware"); + + a = blobmsg_open_array(buf, "id"); + blobmsg_add_u32(buf, NULL, ids.vendor_id); + blobmsg_add_u32(buf, NULL, ids.device_id); + blobmsg_add_u32(buf, NULL, ids.subsystem_vendor_id); + blobmsg_add_u32(buf, NULL, ids.subsystem_device_id); + blobmsg_close_array(buf, a); + + iw_call_str(iw->hardware_name, devname, buf, "name"); + + blobmsg_close_table(buf, o2); + } + + if (!phy_only) { + iw_call_num(iw->quality, devname, buf, "quality"); + iw_call_num(iw->quality_max, devname, buf, "quality_max"); + iw_call_num(iw->bitrate, devname, buf, "bitrate"); + + if (!iw->mode(devname, &nret)) + blobmsg_add_string(buf, "mode", iw_modenames[nret]); + + iw_call_str(iw->ssid, devname, buf, "ssid"); + iw_call_str(iw->bssid, devname, buf, "bssid"); + + if (!iw->encryption(devname, (char *)&crypto)) { + o2 = blobmsg_open_table(buf, "encryption"); + + blobmsg_add_u8(buf, "enabled", crypto.enabled); + + if (crypto.enabled) { + if (!crypto.wpa_version) { + iw_add_bit_array(buf, "wep", crypto.auth_algs, + iw_authnames, IWINFO_AUTH_COUNT, + true, 0); + } else { + a = blobmsg_open_array(buf, "wpa"); + + for (nret = 1; nret <= 3; nret++) + if (crypto.wpa_version & (1 << (nret - 1))) + blobmsg_add_u32(buf, NULL, nret); + + blobmsg_close_array(buf, a); + + iw_add_bit_array(buf, "authentication", + crypto.auth_suites, + iw_kmgmtnames, IWINFO_KMGMT_COUNT, + true, IWINFO_KMGMT_NONE); + } + + iw_add_bit_array(buf, "ciphers", + crypto.pair_ciphers | crypto.group_ciphers, + iw_ciphernames, IWINFO_CIPHER_COUNT, + true, IWINFO_CIPHER_NONE); + } + + blobmsg_close_table(buf, o2); + } + } + + blobmsg_close_table(buf, o); + + iw_close(); + + return true; +} + +static void rpc_luci_get_wireless_devices_cb(struct ubus_request *req, + int type, struct blob_attr *msg) +{ + struct blob_attr *wifi, *cur, *iface, *cur2; + struct reply_context *rctx = req->priv; + const char *name, *first_ifname; + int rem, rem2, rem3, rem4; + void *o, *a, *o2; + + blob_for_each_attr(wifi, msg, rem) { + if (blobmsg_type(wifi) != BLOBMSG_TYPE_TABLE || + blobmsg_name(wifi) == NULL) + continue; + + o = blobmsg_open_table(&rctx->blob, blobmsg_name(wifi)); + + rem2 = blobmsg_data_len(wifi); + first_ifname = NULL; + + __blob_for_each_attr(cur, blobmsg_data(wifi), rem2) { + name = blobmsg_name(cur); + + if (!name || !strcmp(name, "iwinfo")) { + continue; + } + else if (!strcmp(name, "interfaces")) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_ARRAY) + continue; + + a = blobmsg_open_array(&rctx->blob, "interfaces"); + + rem3 = blobmsg_data_len(cur); + + __blob_for_each_attr(iface, blobmsg_data(cur), rem3) { + if (blobmsg_type(iface) != BLOBMSG_TYPE_TABLE) + continue; + + o2 = blobmsg_open_table(&rctx->blob, NULL); + + rem4 = blobmsg_data_len(iface); + name = NULL; + + __blob_for_each_attr(cur2, blobmsg_data(iface), rem4) { + if (!strcmp(blobmsg_name(cur2), "ifname")) + name = blobmsg_get_string(cur2); + else if (!strcmp(blobmsg_name(cur2), "iwinfo")) + continue; + + blobmsg_add_blob(&rctx->blob, cur2); + } + + if (name) + if (rpc_luci_get_iwinfo(&rctx->blob, name, false)) + first_ifname = first_ifname ? first_ifname : name; + + blobmsg_close_table(&rctx->blob, o2); + } + + blobmsg_close_array(&rctx->blob, a); + } + else { + blobmsg_add_blob(&rctx->blob, cur); + } + } + + rpc_luci_get_iwinfo(&rctx->blob, + first_ifname ? first_ifname : blobmsg_name(wifi), + true); + + blobmsg_close_table(&rctx->blob, o); + } + + finish_request(rctx, UBUS_STATUS_OK); +} + +static int +rpc_luci_get_wireless_devices(struct ubus_context *ctx, + struct ubus_object *obj, + struct ubus_request_data *req, + const char *method, + struct blob_attr *msg) +{ + struct reply_context *rctx = defer_request(ctx, req); + + if (!rctx) + return UBUS_STATUS_UNKNOWN_ERROR; + + if (!invoke_ubus(ctx, "network.wireless", "status", NULL, + rpc_luci_get_wireless_devices_cb, rctx)) + return finish_request(rctx, UBUS_STATUS_NOT_FOUND); + + return UBUS_STATUS_OK; +} + +struct host_hint { + struct avl_node avl; + char *hostname; + struct avl_tree ipaddrs; + struct avl_tree ip6addrs; +}; + +/* used to ignore priority with avl_find_element */ +#define HOST_HINT_PRIO_IGNORE -1 + +/* higher (larger) priority addresses are listed first */ +#define HOST_HINT_PRIO_NL 10 /* neighbor table */ +#define HOST_HINT_PRIO_ETHER 50 /* /etc/ethers */ +#define HOST_HINT_PRIO_LEASEFILE 100 /* dhcp leasefile */ +#define HOST_HINT_PRIO_IFADDRS 200 /* getifaddrs() */ +#define HOST_HINT_PRIO_STATIC_LEASE 250 /* uci static leases */ + +struct host_hint_addr { + struct avl_node avl; + int af; + int prio; + union { + struct in_addr in; + struct in6_addr in6; + } addr; +}; + +static int +host_hint_addr_avl_cmp(const void *k1, const void *k2, void *ptr) +{ + struct host_hint_addr *a1 = (struct host_hint_addr *)k1; + struct host_hint_addr *a2 = (struct host_hint_addr *)k2; + + if (a1->prio != a2->prio && + a1->prio != HOST_HINT_PRIO_IGNORE && + a2->prio != HOST_HINT_PRIO_IGNORE) + return a1->prio < a2->prio ? 1 : -1; + + if (a1->af != a2->af) + return a1->af < a2->af ? -1 : 1; + + return memcmp(&a1->addr, &a2->addr, sizeof(a1->addr)); +} + +static int +nl_cb_done(struct nl_msg *msg, void *arg) +{ + struct reply_context *rctx = arg; + rctx->pending = 0; + return NL_STOP; +} + +static int +nl_cb_error(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg) +{ + struct reply_context *rctx = arg; + rctx->pending = 0; + return NL_STOP; +} + +static struct host_hint * +rpc_luci_get_host_hint(struct reply_context *rctx, struct ether_addr *ea) +{ + struct host_hint *hint; + char *p, *mac; + + if (!ea) + return NULL; + + mac = ea2str(ea); + hint = avl_find_element(&rctx->avl, mac, hint, avl); + + if (!hint) { + hint = calloc_a(sizeof(*hint), &p, strlen(mac) + 1); + + if (!hint) + return NULL; + + hint->avl.key = strcpy(p, mac); + avl_init(&hint->ipaddrs, host_hint_addr_avl_cmp, false, NULL); + avl_init(&hint->ip6addrs, host_hint_addr_avl_cmp, false, NULL); + avl_insert(&rctx->avl, &hint->avl); + } + + return hint; +} + +static void +rpc_luci_add_host_hint_addr(struct host_hint *hint, int af, int prio, void *addr) +{ + struct host_hint_addr e, *a; + struct avl_tree *addrs = af == AF_INET ? &hint->ipaddrs : &hint->ip6addrs; + + if (!addr) + return; + + memset(&e, 0, sizeof(e)); + e.af = af; + /* ignore prio when comparing against existing addresses */ + e.prio = HOST_HINT_PRIO_IGNORE; + + if (af == AF_INET) + memcpy(&e.addr.in, (struct in_addr *)addr, sizeof(e.addr.in)); + else + memcpy(&e.addr.in6, (struct in6_addr *)addr, sizeof(e.addr.in6)); + + a = avl_find_element(addrs, &e, a, avl); + + if (a) { + /* update prio of existing address if higher */ + if (prio <= a->prio) + return; + + avl_delete(addrs, &a->avl); + a->prio = prio; + avl_insert(addrs, &a->avl); + return; + } + + a = calloc(1, sizeof(*a)); + + if (!a) + return; + + memcpy(a, &e, sizeof(*a)); + a->prio = prio; + a->avl.key = a; + avl_insert(addrs, &a->avl); +} + +static void +rpc_luci_add_host_hint_ipaddr(struct host_hint *hint, int prio, struct in_addr *addr) +{ + return rpc_luci_add_host_hint_addr(hint, AF_INET, prio, (void *)addr); +} + +static void +rpc_luci_add_host_hint_ip6addr(struct host_hint *hint, int prio, struct in6_addr *addr) +{ + return rpc_luci_add_host_hint_addr(hint, AF_INET6, prio, (void *)addr); +} + +static int nl_cb_dump_neigh(struct nl_msg *msg, void *arg) +{ + struct reply_context *rctx = arg; + struct ether_addr *mac; + struct in6_addr *dst; + struct nlmsghdr *hdr = nlmsg_hdr(msg); + struct ndmsg *nd = NLMSG_DATA(hdr); + struct nlattr *tb[NDA_MAX+1]; + struct host_hint *hint; + + rctx->pending = !!(hdr->nlmsg_flags & NLM_F_MULTI); + + if (hdr->nlmsg_type != RTM_NEWNEIGH || + (nd->ndm_family != AF_INET && nd->ndm_family != AF_INET6)) + return NL_SKIP; + + if (!(nd->ndm_state & (0xFF & ~NUD_NOARP))) + return NL_SKIP; + + nlmsg_parse(hdr, sizeof(*nd), tb, NDA_MAX, NULL); + + mac = tb[NDA_LLADDR] ? RTA_DATA(tb[NDA_LLADDR]) : NULL; + dst = tb[NDA_DST] ? RTA_DATA(tb[NDA_DST]) : NULL; + + if (!mac || !dst) + return NL_SKIP; + + hint = rpc_luci_get_host_hint(rctx, mac); + + if (!hint) + return NL_SKIP; + + if (nd->ndm_family == AF_INET) + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_NL, (struct in_addr *)dst); + else + rpc_luci_add_host_hint_ip6addr(hint, HOST_HINT_PRIO_NL, (struct in6_addr *)dst); + + return NL_SKIP; +} + +static void +rpc_luci_get_host_hints_nl(struct reply_context *rctx) +{ + struct nl_sock *sock = NULL; + struct nl_msg *msg = NULL; + struct nl_cb *cb = NULL; + struct ndmsg ndm = {}; + + sock = nl_socket_alloc(); + + if (!sock) + goto out; + + if (nl_connect(sock, NETLINK_ROUTE)) + goto out; + + cb = nl_cb_alloc(NL_CB_DEFAULT); + + if (!cb) + goto out; + + msg = nlmsg_alloc_simple(RTM_GETNEIGH, NLM_F_REQUEST | NLM_F_DUMP); + + if (!msg) + goto out; + + nlmsg_append(msg, &ndm, sizeof(ndm), 0); + + nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, nl_cb_dump_neigh, rctx); + nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, nl_cb_done, rctx); + nl_cb_err(cb, NL_CB_CUSTOM, nl_cb_error, rctx); + + avl_init(&rctx->avl, avl_strcmp, false, NULL); + + rctx->pending = 1; + + nl_send_auto_complete(sock, msg); + + while (rctx->pending) + nl_recvmsgs(sock, cb); + +out: + if (sock) + nl_socket_free(sock); + + if (cb) + nl_cb_put(cb); + + if (msg) + nlmsg_free(msg); +} + +void rpc_luci_dhcp4_clients(void (*add)(const struct in_addr *, const unsigned char *)) +{ + struct lease_entry *lease; + lease_open(); + while ((lease = lease_next()) != NULL) { + if (lease->af == AF_INET && lease->expire != 0 && !ea_empty(&lease->mac)) + add(&lease->addr[0].in, lease->mac.ether_addr_octet); + } + lease_close(); +} + +/* Discover accounting identities without DNS lookups or browser requests. */ +void rpc_luci_traffic_discover(void (*add)(int, const void *, const unsigned char *)) +{ + struct reply_context rctx = {0}; + struct host_hint *hint, *next; + struct host_hint_addr *addr, *anext; + struct lease_entry *lease; + avl_init(&rctx.avl, avl_strcmp, false, NULL); + rpc_luci_get_host_hints_nl(&rctx); + avl_for_each_element_safe(&rctx.avl, hint, avl, next) { + struct ether_addr *mac = ether_aton(hint->avl.key); + avl_for_each_element_safe(&hint->ipaddrs, addr, avl, anext) { + if (mac) add(AF_INET, &addr->addr.in, mac->ether_addr_octet); + avl_delete(&hint->ipaddrs, &addr->avl); + free(addr); + } + avl_for_each_element_safe(&hint->ip6addrs, addr, avl, anext) { + if (mac) add(AF_INET6, &addr->addr.in6, mac->ether_addr_octet); + avl_delete(&hint->ip6addrs, &addr->avl); + free(addr); + } + avl_delete(&rctx.avl, &hint->avl); + free(hint->hostname); + free(hint); + } + /* Current leases take precedence over stale neighbour cache entries. */ + lease_open(); + while ((lease = lease_next()) != NULL) { + if (lease->expire == 0 || ea_empty(&lease->mac)) continue; + for (int i = 0; i < lease->n_addr; i++) + add(lease->af, &lease->addr[i], lease->mac.ether_addr_octet); + } + lease_close(); +} + +static void +rpc_luci_get_host_hints_ether(struct reply_context *rctx) +{ + struct host_hint *hint; + struct in_addr in; + char buf[512], *p; + FILE *f; + + f = fopen("/etc/ethers", "r"); + + if (!f) + return; + + while (fgets(buf, sizeof(buf), f)) { + p = strtok(buf, " \t\n"); + hint = rpc_luci_get_host_hint(rctx, p ? ether_aton(p) : NULL); + + if (!hint) + continue; + + p = strtok(NULL, " \t\n"); + + if (!p) + continue; + + if (inet_pton(AF_INET, p, &in) == 1) { + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_ETHER, &in); + } + else if (*p && !hint->hostname) { + hint->hostname = strdup(p); + } + } + + fclose(f); +} + +static void +rpc_luci_get_host_hints_uci(struct reply_context *rctx) +{ + struct uci_ptr ptr = { .package = "dhcp" }; + struct uci_context *uci = NULL; + struct uci_package *pkg = NULL; + struct lease_entry *lease; + struct host_hint *hint; + struct uci_element *e, *l; + struct uci_section *s; + struct in_addr in; + char *p, *n; + int i; + + uci = uci_alloc_context(); + + if (!uci) + goto out; + + uci_load(uci, ptr.package, &pkg); + + if (!pkg) + goto out; + + uci_foreach_element(&pkg->sections, e) + { + s = uci_to_section(e); + + if (strcmp(s->type, "host")) + continue; + + ptr.section = s->e.name; + ptr.s = NULL; + + ptr.option = "ip"; + ptr.o = NULL; + + if (!uci_lookup_ptr(uci, &ptr, NULL, true) && ptr.o != NULL && + ptr.o->type != UCI_TYPE_STRING) + n = ptr.o->v.string; + else + n = NULL; + + if (!n || inet_pton(AF_INET, n, &in) != 1) + in.s_addr = 0; + + ptr.option = "name"; + ptr.o = NULL; + + if (!uci_lookup_ptr(uci, &ptr, NULL, true) && ptr.o != NULL && + ptr.o->type == UCI_TYPE_STRING) + n = ptr.o->v.string; + else + n = NULL; + + ptr.option = "mac"; + ptr.o = NULL; + + if (uci_lookup_ptr(uci, &ptr, NULL, true) || ptr.o == NULL) + continue; + + if (ptr.o->type == UCI_TYPE_STRING) { + for (p = strtok(ptr.o->v.string, " \t"); + p != NULL; + p = strtok(NULL, " \t")) { + hint = rpc_luci_get_host_hint(rctx, ether_aton(p)); + + if (!hint) + continue; + + if (in.s_addr != 0) + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_STATIC_LEASE, &in); + + if (n && !hint->hostname) + hint->hostname = strdup(n); + } + } + else if (ptr.o->type == UCI_TYPE_LIST) { + uci_foreach_element(&ptr.o->v.list, l) { + hint = rpc_luci_get_host_hint(rctx, ether_aton(l->name)); + + if (!hint) + continue; + + if (in.s_addr != 0) + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_STATIC_LEASE, &in); + + if (n && !hint->hostname) + hint->hostname = strdup(n); + } + } + } + + lease_open(); + + while ((lease = lease_next()) != NULL) { + if (ea_empty(&lease->mac)) + continue; + + hint = rpc_luci_get_host_hint(rctx, &lease->mac); + + if (!hint) + continue; + + for (i = 0; i < lease->n_addr; i++) { + if (lease->af == AF_INET) + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_LEASEFILE, &lease->addr[i].in); + else if (lease->af == AF_INET6) + rpc_luci_add_host_hint_ip6addr(hint, HOST_HINT_PRIO_LEASEFILE, &lease->addr[i].in6); + } + + if (lease->hostname && !hint->hostname) + hint->hostname = strdup(lease->hostname); + } + + lease_close(); + +out: + if (uci) + uci_free_context(uci); +} + +static void +rpc_luci_get_host_hints_ifaddrs(struct reply_context *rctx) +{ + struct ifaddrs *ifaddr, *ifa; + struct sockaddr_ll *sll; + struct avl_tree devices; + struct host_hint *hint; + struct { + struct avl_node avl; + struct ether_addr ea; + struct in6_addr in6; + struct in_addr in; + } *device, *nextdevice; + char *p; + + avl_init(&devices, avl_strcmp, false, NULL); + + if (getifaddrs(&ifaddr) == -1) + return; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + + device = avl_find_element(&devices, ifa->ifa_name, device, avl); + + if (!device) { + device = calloc_a(sizeof(*device), &p, strlen(ifa->ifa_name) + 1); + + if (!device) + continue; + + device->avl.key = strcpy(p, ifa->ifa_name); + avl_insert(&devices, &device->avl); + } + + switch (ifa->ifa_addr->sa_family) { + case AF_PACKET: + sll = (struct sockaddr_ll *)ifa->ifa_addr; + + if (sll->sll_halen == 6) + memcpy(&device->ea, sll->sll_addr, 6); + + break; + + case AF_INET6: + device->in6 = ((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr; + break; + + case AF_INET: + device->in = ((struct sockaddr_in *)ifa->ifa_addr)->sin_addr; + break; + } + } + + freeifaddrs(ifaddr); + + avl_remove_all_elements(&devices, device, avl, nextdevice) { + if (!ea_empty(&device->ea) && + (!IN6_IS_ADDR_UNSPECIFIED(&device->in6) || + device->in.s_addr != 0)) { + hint = rpc_luci_get_host_hint(rctx, &device->ea); + + if (hint) { + if (device->in.s_addr != 0) + rpc_luci_add_host_hint_ipaddr(hint, HOST_HINT_PRIO_IFADDRS, &device->in); + + if (!IN6_IS_ADDR_UNSPECIFIED(&device->in6)) + rpc_luci_add_host_hint_ip6addr(hint, HOST_HINT_PRIO_IFADDRS, &device->in6); + } + } + + free(device); + } +} + +static int +rpc_luci_get_host_hints_finish(struct reply_context *rctx); + +static void +rpc_luci_get_host_hints_rrdns_cb(struct ubus_request *req, int type, + struct blob_attr *msg) +{ + struct reply_context *rctx = req->priv; + struct host_hint_addr *addr; + struct host_hint *hint; + struct blob_attr *cur; + struct in6_addr in6; + struct in_addr in; + int rem; + + if (msg) { + blob_for_each_attr(cur, msg, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) + continue; + + if (inet_pton(AF_INET6, blobmsg_name(cur), &in6) == 1) { + avl_for_each_element(&rctx->avl, hint, avl) { + avl_for_each_element(&hint->ip6addrs, addr, avl) { + if (!memcmp(&addr->addr.in6, &in6, sizeof(in6))) { + if (!hint->hostname) + hint->hostname = strdup(blobmsg_get_string(cur)); + + break; + } + } + } + } + else if (inet_pton(AF_INET, blobmsg_name(cur), &in) == 1) { + avl_for_each_element(&rctx->avl, hint, avl) { + avl_for_each_element(&hint->ipaddrs, addr, avl) { + if (addr->addr.in.s_addr == in.s_addr) { + free(hint->hostname); + hint->hostname = strdup(blobmsg_get_string(cur)); + break; + } + } + } + } + } + } + + rpc_luci_get_host_hints_finish(rctx); +} + +static void +rpc_luci_get_host_hints_rrdns(struct reply_context *rctx) +{ + char buf[INET6_ADDRSTRLEN]; + struct blob_buf req = {}; + struct host_hint *hint; + struct host_hint_addr *addr; + int n = 0; + void *a; + + blob_buf_init(&req, 0); + + a = blobmsg_open_array(&req, "addrs"); + + avl_for_each_element(&rctx->avl, hint, avl) { + avl_for_each_element(&hint->ipaddrs, addr, avl) { + if (addr->addr.in.s_addr != 0) { + inet_ntop(AF_INET, &addr->addr.in, buf, sizeof(buf)); + blobmsg_add_string(&req, NULL, buf); + n++; + } + } + avl_for_each_element(&hint->ip6addrs, addr, avl) { + if (!IN6_IS_ADDR_UNSPECIFIED(&addr->addr.in6) && + !IN6_IS_ADDR_LINKLOCAL(&addr->addr.in6) && + !IN6_IS_ADDR_ULA(&addr->addr.in6)) { + inet_ntop(AF_INET6, &addr->addr.in6, buf, sizeof(buf)); + blobmsg_add_string(&req, NULL, buf); + n++; + } + } + } + + blobmsg_close_array(&req, a); + + if (n > 0) { + blobmsg_add_u32(&req, "timeout", 250); + blobmsg_add_u32(&req, "limit", n); + + if (!invoke_ubus(rctx->context, "network.rrdns", "lookup", &req, + rpc_luci_get_host_hints_rrdns_cb, rctx)) + rpc_luci_get_host_hints_finish(rctx); + } + else { + rpc_luci_get_host_hints_finish(rctx); + } + + blob_buf_free(&req); +} + +static int +rpc_luci_get_host_hints_finish(struct reply_context *rctx) +{ + struct host_hint *hint, *nexthint; + struct host_hint_addr *addr, *nextaddr; + char buf[INET6_ADDRSTRLEN]; + struct in6_addr in6 = {}; + void *o, *a; + + avl_remove_all_elements(&rctx->avl, hint, avl, nexthint) { + o = blobmsg_open_table(&rctx->blob, hint->avl.key); + + a = blobmsg_open_array(&rctx->blob, "ipaddrs"); + + avl_remove_all_elements(&hint->ipaddrs, addr, avl, nextaddr) { + if (addr->addr.in.s_addr != 0) { + inet_ntop(AF_INET, &addr->addr.in, buf, sizeof(buf)); + blobmsg_add_string(&rctx->blob, NULL, buf); + } + + free(addr); + } + + blobmsg_close_array(&rctx->blob, a); + + a = blobmsg_open_array(&rctx->blob, "ip6addrs"); + + avl_remove_all_elements(&hint->ip6addrs, addr, avl, nextaddr) { + if (memcmp(&addr->addr.in6, &in6, sizeof(in6))) { + inet_ntop(AF_INET6, &addr->addr.in6, buf, sizeof(buf)); + blobmsg_add_string(&rctx->blob, NULL, buf); + } + + free(addr); + } + + blobmsg_close_array(&rctx->blob, a); + + if (hint->hostname) + blobmsg_add_string(&rctx->blob, "name", hint->hostname); + + blobmsg_close_table(&rctx->blob, o); + + if (hint->hostname) + free(hint->hostname); + + free(hint); + } + + return finish_request(rctx, UBUS_STATUS_OK); +} + +static int +rpc_luci_get_host_hints(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct reply_context *rctx = defer_request(ctx, req); + + if (!rctx) + return UBUS_STATUS_UNKNOWN_ERROR; + + rpc_luci_get_host_hints_nl(rctx); + rpc_luci_get_host_hints_uci(rctx); + rpc_luci_get_host_hints_ether(rctx); + rpc_luci_get_host_hints_ifaddrs(rctx); + rpc_luci_get_host_hints_rrdns(rctx); + + return UBUS_STATUS_OK; +} + +static int +rpc_luci_get_duid_hints(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct { struct avl_node avl; } *e, *next; + char s[INET6_ADDRSTRLEN], *p; + struct lease_entry *lease; + struct avl_tree avl; + void *o, *a; + int n; + + avl_init(&avl, avl_strcmp, false, NULL); + blob_buf_init(&blob, 0); + + lease_open(); + + while ((lease = lease_next()) != NULL) { + if (lease->af != AF_INET6 || lease->duid == NULL) + continue; + + size_t key_len = strlen(lease->duid) + 1 + (lease->iaid ? strlen(lease->iaid) + 1 : 0); + char key[key_len]; + + sprintf(key, "%s%s%s", lease->duid, + lease->iaid ? "%" : "", + lease->iaid ? lease->iaid : ""); + + e = avl_find_element(&avl, key, e, avl); + if (e) + continue; + + e = calloc_a(sizeof(*e), &p, key_len); + if (!e) + continue; + + o = blobmsg_open_table(&blob, key); + + if (lease->iface) + blobmsg_add_string(&blob, "interface", lease->iface); + + blobmsg_add_string(&blob, "duid", lease->duid); + + if (lease->iaid) + blobmsg_add_string(&blob, "iaid", lease->iaid); + + if (lease->hostname) + blobmsg_add_string(&blob, "hostname", lease->hostname); + + if (!ea_empty(&lease->mac)) + blobmsg_add_string(&blob, "macaddr", ea2str(&lease->mac)); + + a = blobmsg_open_array(&blob, "ip6addrs"); + for (n = 0; n < lease->n_addr; n++) { + inet_ntop(AF_INET6, &lease->addr[n].in6, s, sizeof(s)); + blobmsg_add_string(&blob, NULL, s); + } + blobmsg_close_array(&blob, a); + + blobmsg_close_table(&blob, o); + + e->avl.key = strcpy(p, key); + avl_insert(&avl, &e->avl); + } + + lease_close(); + + avl_remove_all_elements(&avl, e, avl, next) { + free(e); + } + + ubus_send_reply(ctx, req, blob.head); + + return UBUS_STATUS_OK; +} + +static int +rpc_luci_get_board_json(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + blob_buf_init(&blob, 0); + + if (!blobmsg_add_json_from_file(&blob, "/etc/board.json")) + return UBUS_STATUS_UNKNOWN_ERROR; + + ubus_send_reply(ctx, req, blob.head); + return UBUS_STATUS_OK; +} + +enum { + RPC_L_FAMILY, + __RPC_L_MAX, +}; + +static const struct blobmsg_policy rpc_get_leases_policy[__RPC_L_MAX] = { + [RPC_L_FAMILY] = { .name = "family", .type = BLOBMSG_TYPE_INT32 } +}; + +static int +rpc_luci_get_dhcp_leases(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + char s[INET6_ADDRSTRLEN + strlen("/128")]; + struct blob_attr *tb[__RPC_L_MAX]; + struct lease_entry *lease; + int af, family = 0; + void *a, *a2, *o; + size_t l; + int n; + + blobmsg_parse(rpc_get_leases_policy, __RPC_L_MAX, tb, + blob_data(msg), blob_len(msg)); + + switch (tb[RPC_L_FAMILY] ? blobmsg_get_u32(tb[RPC_L_FAMILY]) : 0) { + case 0: + family = 0; + break; + + case 4: + family = AF_INET; + break; + + case 6: + family = AF_INET6; + break; + + default: + return UBUS_STATUS_INVALID_ARGUMENT; + } + + blob_buf_init(&blob, 0); + + for (af = family ? family : AF_INET; + af != 0; + af = (family == 0) ? (af == AF_INET ? AF_INET6 : 0) : 0) { + + a = blobmsg_open_array(&blob, (af == AF_INET) ? "dhcp_leases" + : "dhcp6_leases"); + + lease_open(); + + while ((lease = lease_next()) != NULL) { + if (lease->af != af) + continue; + + o = blobmsg_open_table(&blob, NULL); + + if (lease->expire == -1) + blobmsg_add_u8(&blob, "expires", 0); + else + blobmsg_add_u32(&blob, "expires", lease->expire); + + if (lease->iface) + blobmsg_add_string(&blob, "interface", lease->iface); + + if (lease->hostname) + blobmsg_add_string(&blob, "hostname", lease->hostname); + + if (!ea_empty(&lease->mac)) + blobmsg_add_string(&blob, "macaddr", ea2str(&lease->mac)); + + if (lease->duid) + blobmsg_add_string(&blob, "duid", lease->duid); + + if (lease->iaid) + blobmsg_add_string(&blob, "iaid", lease->iaid); + + inet_ntop(lease->af, &lease->addr[0].in6, s, sizeof(s)); + blobmsg_add_string(&blob, (af == AF_INET) ? "ipaddr" : "ip6addr", s); + + if (af == AF_INET6) { + a2 = blobmsg_open_array(&blob, "ip6addrs"); + + for (n = 0; n < lease->n_addr; n++) { + inet_ntop(lease->af, &lease->addr[n].in6, s, sizeof(s)); + + l = strlen(s); + snprintf(s + l, sizeof(s) - l, "/%hhu", lease->mask); + + blobmsg_add_string(&blob, NULL, s); + } + + blobmsg_close_array(&blob, a2); + } + + blobmsg_close_table(&blob, o); + } + + lease_close(); + + blobmsg_close_array(&blob, a); + } + + ubus_send_reply(ctx, req, blob.head); + + return UBUS_STATUS_OK; +} + +static int +rpc_luci_api_init(const struct rpc_daemon_ops *o, struct ubus_context *ctx) +{ + static const struct ubus_method luci_methods[] = { + UBUS_METHOD_NOARG("getNetworkDevices", rpc_luci_get_network_devices), + UBUS_METHOD_NOARG("getWirelessDevices", rpc_luci_get_wireless_devices), + UBUS_METHOD_NOARG("getHostHints", rpc_luci_get_host_hints), + UBUS_METHOD_NOARG("getDUIDHints", rpc_luci_get_duid_hints), + UBUS_METHOD_NOARG("getBoardJSON", rpc_luci_get_board_json), + UBUS_METHOD("getDHCPLeases", rpc_luci_get_dhcp_leases, rpc_get_leases_policy) + }; + + static struct ubus_object_type luci_type = + UBUS_OBJECT_TYPE("rpcd-luci", luci_methods); + + static struct ubus_object obj = { + .name = "luci-rpc", + .type = &luci_type, + .methods = luci_methods, + .n_methods = ARRAY_SIZE(luci_methods), + }; + + int ret = ubus_add_object(ctx, &obj); + if (!ret) + ret = rpc_luci_traffic_init(ctx); + if (!ret) + ret = rpc_luci_webprobe_init(ctx); + return ret; +} + +struct rpc_plugin rpc_plugin = { + .init = rpc_luci_api_init +}; diff --git a/rpcd-mod-luci/src/traffic.c b/rpcd-mod-luci/src/traffic.c new file mode 100644 index 000000000..8ab9de1f3 --- /dev/null +++ b/rpcd-mod-luci/src/traffic.c @@ -0,0 +1,781 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Read-only conntrack accounting, including ECM/NSS synchronized counters. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SAMPLE_MS 2000 +#define WINDOW_SAMPLES 3 +#define ADDRESS_IDLE_MS (24ULL * 60 * 60 * 1000) +#define MAX_HOSTS 4096 +#ifndef STATE_FILE +#define STATE_FILE "/tmp/luci-client-traffic.state" +#endif +#define MAX_CLIENTS 8192 +#define MAX_REQUEST 1024 +#define MAX_FLOWS 32768 + +struct address { uint8_t family, bytes[16]; }; +struct flow_key { + struct address src, dst, reply_src; + uint32_t id; + uint16_t sport, dport, zone; + uint8_t protocol; +}; +struct host { + struct avl_node avl; + unsigned char mac[6]; + uint64_t total[2]; + uint32_t connections, pending_connections; + bool available; +}; +struct flow { + struct avl_node avl; + struct flow_key key; + uint64_t bytes[2]; + unsigned char owner[2][6]; + uint32_t generation; + bool destroyed; +}; +struct client { + struct avl_node avl; + struct address address; + unsigned char mac[6]; + uint64_t bytes[2], rate[2], touched; + uint64_t history[WINDOW_SAMPLES][2], duration[WINDOW_SAMPLES]; + unsigned int next_sample; + bool warm, ready, missing; +}; +static struct avl_tree clients, flows, hosts; +static struct uloop_fd ct_fd = { .fd = -1 }; +static struct uloop_timeout timer; +static uint32_t sequence, generation; +static uint64_t dump_started, sampled, started; +static bool incomplete; +static bool dumping, initialized; +static const char *failure = "warming_up"; +static struct blob_buf result; + +int rpc_luci_traffic_init(struct ubus_context *ctx); +void rpc_luci_traffic_discover(void (*add)(int, const void *, const unsigned char *)); +static void save_state(void); +static void load_state(void); + +static uint64_t now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int address_cmp(const void *a, const void *b, void *priv) +{ + return memcmp(a, b, sizeof(struct address)); +} + +static int flow_cmp(const void *a, const void *b, void *priv) +{ + return memcmp(a, b, sizeof(struct flow_key)); +} + +static bool parse_address(const char *text, struct address *a) +{ + memset(a, 0, sizeof(*a)); + if (inet_pton(AF_INET, text, a->bytes) == 1) { + a->family = AF_INET; + return true; + } + if (inet_pton(AF_INET6, text, a->bytes) == 1) { + a->family = AF_INET6; + return true; + } + return false; +} + +static int mac_cmp(const void *a, const void *b, void *priv) +{ + return memcmp(a, b, 6); +} + +static struct host *get_host(const unsigned char *mac, bool create) +{ + static const unsigned char zero[6]; + struct host *h; + if (!memcmp(mac, zero, 6) || (mac[0] & 1)) + return NULL; + h = avl_find_element(&hosts, mac, h, avl); + if (!h && create) { + if (hosts.count >= MAX_HOSTS || !(h = calloc(1, sizeof(*h)))) { + incomplete = true; + return NULL; + } + memcpy(h->mac, mac, 6); + h->avl.key = h->mac; + avl_insert(&hosts, &h->avl); + } + return h; +} + +static void bind_client(int family, const void *bytes, const unsigned char *mac) +{ + struct address a = { .family = family }; + struct client *c; + if ((family != AF_INET && family != AF_INET6) || !get_host(mac, true)) + return; + memcpy(a.bytes, bytes, family == AF_INET ? 4 : 16); + c = avl_find_element(&clients, &a, c, avl); + if (!c) { + if (clients.count >= MAX_CLIENTS || !(c = calloc(1, sizeof(*c)))) { + incomplete = true; + return; + } + c->address = a; + c->avl.key = &c->address; + avl_insert(&clients, &c->avl); + } + if (memcmp(c->mac, mac, 6)) { + memcpy(c->mac, mac, 6); + c->warm = c->ready = false; + memset(c->bytes, 0, sizeof(c->bytes)); + memset(c->history, 0, sizeof(c->history)); + memset(c->duration, 0, sizeof(c->duration)); + c->next_sample = 0; + } + c->touched = now_ms(); +} + +/* Apply one final binding per address: a stale neighbour and a current DHCP + * lease must not reset the same client's rate baseline twice per scan. */ +struct binding { struct avl_node avl; struct address address; unsigned char mac[6]; }; +static struct avl_tree bindings; + +static void stage_binding(int family, const void *bytes, const unsigned char *mac) +{ + struct address address = { .family = family }; + struct binding *b; + if (family != AF_INET && family != AF_INET6) return; + memcpy(address.bytes, bytes, family == AF_INET ? 4 : 16); + b = avl_find_element(&bindings, &address, b, avl); + if (!b) { + if (bindings.count >= MAX_CLIENTS || !(b = calloc(1, sizeof(*b)))) { + incomplete = true; + return; + } + b->address = address; + b->avl.key = &b->address; + avl_insert(&bindings, &b->avl); + } + memcpy(b->mac, mac, 6); +} + +static void discover_clients(void) +{ + struct binding *b, *next; + avl_init(&bindings, address_cmp, false, NULL); + rpc_luci_traffic_discover(stage_binding); + avl_for_each_element_safe(&bindings, b, avl, next) { + bind_client(b->address.family, b->address.bytes, b->mac); + avl_delete(&bindings, &b->avl); + free(b); + } +} + +static void clear_flows(void) +{ + struct flow *f, *tmp; + avl_for_each_element_safe(&flows, f, avl, tmp) { + avl_delete(&flows, &f->avl); + free(f); + } +} + +static void reset_sampler(const char *reason) +{ + struct client *c; + if (ct_fd.fd >= 0) { + uloop_fd_delete(&ct_fd); + close(ct_fd.fd); + ct_fd.fd = -1; + } + incomplete = true; + avl_for_each_element(&clients, c, avl) { + c->warm = c->ready = false; + c->bytes[0] = c->bytes[1] = 0; + memset(c->history, 0, sizeof(c->history)); + memset(c->duration, 0, sizeof(c->duration)); + c->next_sample = 0; + } + dumping = false; + sampled = 0; + failure = reason; +} + +/* A complete checkpoint lives in tmpfs, never on flash. Keep flow baselines + * together with totals so an rpcd reload cannot count live connections twice. */ +struct state_header { + char magic[8], boot[40]; + uint64_t started; + uint32_t hosts, clients, flows, generation, incomplete; +}; +struct host_record { unsigned char mac[6]; uint64_t total[2]; bool available; }; +struct client_record { struct address address; unsigned char mac[6]; uint64_t touched; }; +struct flow_record { + struct flow_key key; + uint64_t bytes[2]; + unsigned char owner[2][6]; + uint32_t generation; + bool destroyed; +}; + +static void boot_id(char *out, size_t len) +{ + FILE *f = fopen("/proc/sys/kernel/random/boot_id", "r"); + if (f) { if (!fgets(out, len, f)) out[0] = 0; fclose(f); } +} + +static void save_state(void) +{ + struct state_header header = { .magic = "LCTRAF1", .started = started, + .hosts = hosts.count, .clients = clients.count, .flows = flows.count, + .generation = generation, .incomplete = incomplete }; + struct host *h; + struct client *c; + struct flow *flow; + bool ok = true; + boot_id(header.boot, sizeof(header.boot)); + if (!header.boot[0]) return; + int fd = open(STATE_FILE ".new", O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) return; + FILE *f = fdopen(fd, "wb"); + if (!f) { close(fd); return; } + ok = fwrite(&header, sizeof(header), 1, f) == 1; + avl_for_each_element(&hosts, h, avl) { + struct host_record r = {0}; + memcpy(r.mac, h->mac, 6); + memcpy(r.total, h->total, sizeof(r.total)); + r.available = h->available; + ok &= fwrite(&r, sizeof(r), 1, f) == 1; + } + avl_for_each_element(&clients, c, avl) { + struct client_record r = { .address = c->address, .touched = c->touched }; + memcpy(r.mac, c->mac, 6); + ok &= fwrite(&r, sizeof(r), 1, f) == 1; + } + avl_for_each_element(&flows, flow, avl) { + struct flow_record r = { .key = flow->key, .generation = flow->generation, + .destroyed = flow->destroyed }; + memcpy(r.bytes, flow->bytes, sizeof(r.bytes)); + memcpy(r.owner, flow->owner, sizeof(r.owner)); + ok &= fwrite(&r, sizeof(r), 1, f) == 1; + } + if (fclose(f)) ok = false; + if (ok) rename(STATE_FILE ".new", STATE_FILE); + else unlink(STATE_FILE ".new"); +} + +static void load_state(void) +{ + struct state_header header; + char boot[40] = {0}; + struct stat st; + int fd = open(STATE_FILE, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return; + FILE *f = fdopen(fd, "rb"); + if (!f) { close(fd); return; } + boot_id(boot, sizeof(boot)); + if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_uid != geteuid() || + fread(&header, sizeof(header), 1, f) != 1 || + memcmp(header.magic, "LCTRAF1", 8) || !boot[0] || memcmp(header.boot, boot, sizeof(boot)) || + header.hosts > MAX_HOSTS || header.clients > MAX_CLIENTS || header.flows > MAX_FLOWS || + header.started > now_ms() || st.st_size != sizeof(header) + + header.hosts * sizeof(struct host_record) + header.clients * sizeof(struct client_record) + + header.flows * sizeof(struct flow_record)) goto out; + for (uint32_t i = 0; i < header.hosts; i++) { + struct host_record r; + struct host *h; + if (fread(&r, sizeof(r), 1, f) != 1 || !(h = get_host(r.mac, true))) goto fail; + memcpy(h->total, r.total, sizeof(h->total)); + h->available = r.available; + } + for (uint32_t i = 0; i < header.clients; i++) { + struct client_record r; + struct client *c; + if (fread(&r, sizeof(r), 1, f) != 1) goto fail; + bind_client(r.address.family, r.address.bytes, r.mac); + c = avl_find_element(&clients, &r.address, c, avl); + if (!c) goto fail; + c->touched = r.touched; + } + for (uint32_t i = 0; i < header.flows; i++) { + struct flow_record r; + struct flow *flow; + if (fread(&r, sizeof(r), 1, f) != 1 || !(flow = calloc(1, sizeof(*flow)))) goto fail; + flow->key = r.key; + flow->avl.key = &flow->key; + memcpy(flow->bytes, r.bytes, sizeof(flow->bytes)); + memcpy(flow->owner, r.owner, sizeof(flow->owner)); + flow->generation = r.generation; + flow->destroyed = r.destroyed; + if (avl_insert(&flows, &flow->avl)) { free(flow); goto fail; } + } + started = header.started; + generation = header.generation; + /* Connections ending while rpcd was stopped cannot be recovered. */ + incomplete = true; + goto out; +fail:; + struct client *c, *cn; + struct host *h, *hn; + clear_flows(); + avl_for_each_element_safe(&clients, c, avl, cn) { avl_delete(&clients, &c->avl); free(c); } + avl_for_each_element_safe(&hosts, h, avl, hn) { avl_delete(&hosts, &h->avl); free(h); } + incomplete = true; +out: + fclose(f); +} + +/* All attributes come from the kernel, but validate lengths before reading. */ +static struct nlattr *attr_find(void *data, size_t len, unsigned int type) +{ + struct nlattr *a = data; + while (len >= sizeof(*a) && a->nla_len >= sizeof(*a) && a->nla_len <= len) { + if ((a->nla_type & NLA_TYPE_MASK) == type) + return a; + size_t step = NLA_ALIGN(a->nla_len); + if (step > len) + break; + len -= step; + a = (void *)((char *)a + step); + } + return NULL; +} + +static struct nlattr *nested(struct nlattr *a, unsigned int type) +{ + return a ? attr_find((char *)a + NLA_HDRLEN, a->nla_len - NLA_HDRLEN, type) : NULL; +} + +static bool attr_copy(struct nlattr *a, void *out, size_t len) +{ + if (!a || a->nla_len < NLA_HDRLEN + len) + return false; + memcpy(out, (char *)a + NLA_HDRLEN, len); + return true; +} + +static bool tuple_address(struct nlattr *tuple, unsigned int family, bool source, struct address *addr) +{ + struct nlattr *ip = nested(tuple, CTA_TUPLE_IP); + unsigned int type = family == AF_INET ? (source ? CTA_IP_V4_SRC : CTA_IP_V4_DST) : + (source ? CTA_IP_V6_SRC : CTA_IP_V6_DST); + addr->family = family; + return attr_copy(nested(ip, type), addr->bytes, family == AF_INET ? 4 : 16); +} + +static bool counter_value(struct nlattr *a, uint64_t *value) +{ + unsigned char bytes[8]; + if (!attr_copy(nested(a, CTA_COUNTERS_BYTES), bytes, sizeof(bytes))) + return false; + *value = 0; + for (int i = 0; i < 8; i++) + *value = (*value << 8) | bytes[i]; + return true; +} + +static void add_bytes(struct client *c, const unsigned char *owner, uint64_t up, uint64_t down, bool count) +{ + struct host *h = get_host(owner, false); + if (h) h->available = true; + if (count && h) { + h->total[0] += up; + h->total[1] += down; + } + if (c && c->warm && !memcmp(c->mac, owner, 6)) { + c->bytes[0] += up; + c->bytes[1] += down; + } +} + +/* Count dump records independently of byte accounting and flow tombstones. */ +static void count_connection(struct nlmsghdr *nlh) +{ + struct nfgenmsg *msg = NLMSG_DATA(nlh); + struct address orig_src = {0}, reply_src = {0}; + if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*msg)) || + (msg->nfgen_family != AF_INET && msg->nfgen_family != AF_INET6)) + return; + void *data = (char *)msg + NLMSG_ALIGN(sizeof(*msg)); + size_t len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*msg)); + if (!tuple_address(attr_find(data, len, CTA_TUPLE_ORIG), msg->nfgen_family, true, &orig_src) || + !tuple_address(attr_find(data, len, CTA_TUPLE_REPLY), msg->nfgen_family, true, &reply_src)) + return; + struct client *src = avl_find_element(&clients, &orig_src, src, avl); + struct client *dst = avl_find_element(&clients, &reply_src, dst, avl); + struct host *a = src ? get_host(src->mac, false) : NULL; + struct host *b = dst ? get_host(dst->mac, false) : NULL; + if (a) a->pending_connections++; + /* Hairpin and dual-address connections count once for the same MAC. */ + if (b && b != a) b->pending_connections++; +} + +static bool process_flow(struct nlmsghdr *nlh) +{ + struct nfgenmsg *msg = NLMSG_DATA(nlh); + struct flow_key key = {0}; + struct flow *f; + struct client *src, *dst; + uint64_t bytes[2], delta[2] = {0}; + bool known; + bool destroyed = (nlh->nlmsg_type & 0xff) == IPCTNL_MSG_CT_DELETE; + if (nlh->nlmsg_len < NLMSG_LENGTH(sizeof(*msg)) || + (msg->nfgen_family != AF_INET && msg->nfgen_family != AF_INET6)) + return true; + void *data = (char *)msg + NLMSG_ALIGN(sizeof(*msg)); + size_t len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*msg)); + struct nlattr *orig = attr_find(data, len, CTA_TUPLE_ORIG); + struct nlattr *reply = attr_find(data, len, CTA_TUPLE_REPLY); + struct nlattr *proto = nested(orig, CTA_TUPLE_PROTO); + if (!tuple_address(orig, msg->nfgen_family, true, &key.src) || + !tuple_address(orig, msg->nfgen_family, false, &key.dst) || + !tuple_address(reply, msg->nfgen_family, true, &key.reply_src) || + !attr_copy(nested(proto, CTA_PROTO_NUM), &key.protocol, 1) || + !attr_copy(attr_find(data, len, CTA_ID), &key.id, 4)) + return true; + attr_copy(nested(proto, CTA_PROTO_SRC_PORT), &key.sport, 2); + attr_copy(nested(proto, CTA_PROTO_DST_PORT), &key.dport, 2); + attr_copy(attr_find(data, len, CTA_ZONE), &key.zone, 2); + src = avl_find_element(&clients, &key.src, src, avl); + /* reply.src identifies the internal destination of a DNAT connection. */ + dst = avl_find_element(&clients, &key.reply_src, dst, avl); + f = avl_find_element(&flows, &key, f, avl); + known = f != NULL; + if (!src && !dst && !f) + return true; + if (!counter_value(attr_find(data, len, CTA_COUNTERS_ORIG), &bytes[0]) || + !counter_value(attr_find(data, len, CTA_COUNTERS_REPLY), &bytes[1])) { + if (src) src->missing = true; + if (dst) dst->missing = true; + incomplete = true; + return true; + } + if (!f) { + if (flows.count >= MAX_FLOWS) + return false; + f = calloc(1, sizeof(*f)); + if (!f) + return false; + f->key = key; + if (src) memcpy(f->owner[0], src->mac, 6); + if (dst) memcpy(f->owner[1], dst->mac, 6); + f->avl.key = &f->key; + avl_insert(&flows, &f->avl); + } + /* A dump record can be queued before a destroy notification is delivered. + * Keep tombstones for two rounds to avoid counting that old record twice. */ + if (f->destroyed) + return true; + for (int i = 0; i < 2; i++) { + /* A counter reset starts a new baseline; never subtract unsigned values. */ + if (bytes[i] >= f->bytes[i]) + delta[i] = bytes[i] - f->bytes[i]; + f->bytes[i] = bytes[i]; + } + add_bytes(src, f->owner[0], delta[0], delta[1], known || (src && src->warm)); + if (memcmp(f->owner[0], f->owner[1], 6)) + add_bytes(dst, f->owner[1], delta[1], delta[0], known || (dst && dst->warm)); + f->generation = generation; + if (destroyed) + f->destroyed = true; + return true; +} + +/* Weight by actual elapsed time, not an average of rounded sample rates. */ +static void update_rate(struct client *c, uint64_t elapsed) +{ + if (!c->ready) { + memset(c->history, 0, sizeof(c->history)); + memset(c->duration, 0, sizeof(c->duration)); + c->next_sample = 0; + } else { + memcpy(c->history[c->next_sample], c->bytes, sizeof(c->bytes)); + c->duration[c->next_sample] = elapsed; + c->next_sample = (c->next_sample + 1) % WINDOW_SAMPLES; + } + uint64_t duration = 0, bytes[2] = {0}; + for (int j = 0; j < WINDOW_SAMPLES; j++) { + duration += c->duration[j]; + bytes[0] += c->history[j][0]; + bytes[1] += c->history[j][1]; + } + for (int i = 0; i < 2; i++) { + c->rate[i] = duration ? (uint64_t)((double)bytes[i] * 1000 / duration) : 0; + c->bytes[i] = 0; + } +} + +static void finish_dump(void) +{ + struct host *h; + avl_for_each_element(&hosts, h, avl) + h->connections = h->pending_connections; + uint64_t now = now_ms(), elapsed = now - sampled; + struct client *c; + struct flow *f, *tmp; + avl_for_each_element(&clients, c, avl) { + c->ready = c->warm && sampled && elapsed && !c->missing; + update_rate(c, elapsed); + c->warm = true; + c->missing = false; + } + avl_for_each_element_safe(&flows, f, avl, tmp) { + if ((uint32_t)(generation - f->generation) > 1) { + avl_delete(&flows, &f->avl); + free(f); + } + } + sampled = now; + dumping = false; + failure = NULL; + save_state(); +} + +static void receive_conntrack(struct uloop_fd *fd, unsigned int events) +{ + /* Bound each callback so a large table cannot starve other rpcd requests. */ + for (int batch = 0; batch < 32; batch++) { + char buf[65536]; + struct sockaddr_nl sender; + struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) }; + struct msghdr msg = { .msg_name = &sender, .msg_namelen = sizeof(sender), + .msg_iov = &iov, .msg_iovlen = 1 }; + ssize_t len = recvmsg(fd->fd, &msg, MSG_DONTWAIT); + if (len < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return; + reset_sampler("netlink_overrun"); + return; + } + if (!len || (msg.msg_flags & MSG_TRUNC)) { + reset_sampler("truncated_dump"); + return; + } + if (sender.nl_pid) + continue; + struct nlmsghdr *nlh; + for (nlh = (void *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { + bool response = dumping && nlh->nlmsg_seq == sequence; + if (response && (nlh->nlmsg_flags & NLM_F_DUMP_INTR)) { + reset_sampler("interrupted_dump"); + return; + } + if (nlh->nlmsg_type == NLMSG_ERROR) { + reset_sampler("netlink_error"); + return; + } + if (nlh->nlmsg_type == NLMSG_DONE) { + if (response) finish_dump(); + continue; + } + if ((nlh->nlmsg_type >> 8) != NFNL_SUBSYS_CTNETLINK) + continue; + if (response && (nlh->nlmsg_type & 0xff) == IPCTNL_MSG_CT_NEW) + count_connection(nlh); + if (response || (nlh->nlmsg_type & 0xff) == IPCTNL_MSG_CT_DELETE) { + if (!process_flow(nlh)) { + reset_sampler("flow_limit"); + /* Drop baselines only at capacity, allowing a later smaller dump + * to recover. Cold clients will establish a fresh baseline. */ + clear_flows(); + return; + } + } + } + } +} + +static bool open_conntrack(void) +{ + struct sockaddr_nl addr = { .nl_family = AF_NETLINK, + .nl_groups = 1 << (NFNLGRP_CONNTRACK_DESTROY - 1) }; + int size = 1024 * 1024; + ct_fd.fd = socket(AF_NETLINK, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, NETLINK_NETFILTER); + if (ct_fd.fd < 0) + return false; + setsockopt(ct_fd.fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)); + if (bind(ct_fd.fd, (void *)&addr, sizeof(addr)) < 0) { + close(ct_fd.fd); + ct_fd.fd = -1; + return false; + } + ct_fd.cb = receive_conntrack; + if (uloop_fd_add(&ct_fd, ULOOP_READ) < 0) { + close(ct_fd.fd); + ct_fd.fd = -1; + return false; + } + return true; +} + +static void sample_timer(struct uloop_timeout *t) +{ + uint64_t now = now_ms(); + struct client *c, *tmp; + avl_for_each_element_safe(&clients, c, avl, tmp) { + if (now - c->touched > ADDRESS_IDLE_MS) { + avl_delete(&clients, &c->avl); + free(c); + } + } + uloop_timeout_set(&timer, SAMPLE_MS); + if (dumping) { + if (now - dump_started > 10000) + reset_sampler("dump_timeout"); + return; + } + discover_clients(); + FILE *acct = fopen("/proc/sys/net/netfilter/nf_conntrack_acct", "r"); + int enabled = 0; + if (acct) { enabled = fgetc(acct) == '1'; fclose(acct); } + if (!enabled) { + reset_sampler("accounting_disabled"); + uloop_timeout_set(&timer, 10000); + return; + } + if (ct_fd.fd < 0 && !open_conntrack()) { + failure = "netlink_unavailable"; + uloop_timeout_set(&timer, 10000); + return; + } + struct { struct nlmsghdr nlh; struct nfgenmsg nfg; } request = { + .nlh = { .nlmsg_len = NLMSG_LENGTH(sizeof(struct nfgenmsg)), + .nlmsg_type = (NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, .nlmsg_seq = ++sequence }, + .nfg = { .nfgen_family = AF_UNSPEC, .version = NFNETLINK_V0 } + }; + struct sockaddr_nl kernel = { .nl_family = AF_NETLINK }; + /* Never use CT_GET_CTRZERO: other accounting consumers keep their counters. */ + if (sendto(ct_fd.fd, &request, request.nlh.nlmsg_len, MSG_DONTWAIT, + (void *)&kernel, sizeof(kernel)) < 0) { + reset_sampler("send_failed"); + return; + } + struct host *h; + avl_for_each_element(&hosts, h, avl) + h->pending_connections = 0; + generation++; + dumping = true; + dump_started = now; +} + +static const struct blobmsg_policy policy[] = { + { .name = "addresses", .type = BLOBMSG_TYPE_ARRAY } +}; + +static int get_rates(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + struct blob_attr *tb[1], *a; + struct address address; + struct client *c; + uint64_t now = now_ms(); + int rem, count = 0; + if (!msg) + return UBUS_STATUS_INVALID_ARGUMENT; + blobmsg_parse(policy, 1, tb, blob_data(msg), blob_len(msg)); + if (!tb[0]) + return UBUS_STATUS_INVALID_ARGUMENT; + blobmsg_for_each_attr(a, tb[0], rem) { + if (++count > MAX_REQUEST || blobmsg_type(a) != BLOBMSG_TYPE_STRING || + !parse_address(blobmsg_get_string(a), &address)) + return UBUS_STATUS_INVALID_ARGUMENT; + } + blob_buf_init(&result, 0); + blobmsg_add_string(&result, "source", "conntrack"); + blobmsg_add_u64(&result, "started_ms", started); + blobmsg_add_u8(&result, "incomplete", incomplete); + blobmsg_add_u32(&result, "interval_ms", SAMPLE_MS); + blobmsg_add_u32(&result, "window_ms", SAMPLE_MS * WINDOW_SAMPLES); + blobmsg_add_u32(&result, "age_ms", sampled ? now - sampled : 0); + if (failure) + blobmsg_add_string(&result, "status", failure); + void *table = blobmsg_open_table(&result, "rates"); + blobmsg_for_each_attr(a, tb[0], rem) { + parse_address(blobmsg_get_string(a), &address); + c = avl_find_element(&clients, &address, c, avl); + void *entry = blobmsg_open_table(&result, blobmsg_get_string(a)); + bool ready = c && c->ready && sampled && now - sampled < 3 * SAMPLE_MS; + blobmsg_add_u8(&result, "ready", ready); + struct host *h = c ? get_host(c->mac, false) : NULL; + if (h) { + char mac[18]; + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + h->mac[0], h->mac[1], h->mac[2], h->mac[3], h->mac[4], h->mac[5]); + blobmsg_add_string(&result, "mac", mac); + } + if (ready) { + blobmsg_add_u64(&result, "upload", c->rate[0]); + blobmsg_add_u64(&result, "download", c->rate[1]); + } + blobmsg_close_table(&result, entry); + } + blobmsg_close_table(&result, table); + void *totals = blobmsg_open_table(&result, "totals"); + struct host *host; + avl_for_each_element(&hosts, host, avl) { + if (!host->available && !sampled) continue; + char mac[18]; + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + host->mac[0], host->mac[1], host->mac[2], host->mac[3], host->mac[4], host->mac[5]); + blobmsg_add_u64(&result, mac, host->total[0] + host->total[1]); + } + blobmsg_close_table(&result, totals); + + void *connections = blobmsg_open_table(&result, "connections"); + if (sampled && !failure && now - sampled < 3 * SAMPLE_MS) { + avl_for_each_element(&hosts, host, avl) { + char mac[18]; + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + host->mac[0], host->mac[1], host->mac[2], host->mac[3], host->mac[4], host->mac[5]); + blobmsg_add_u32(&result, mac, host->connections); + } + } + blobmsg_close_table(&result, connections); + ubus_send_reply(ctx, req, result.head); + return 0; +} + +int rpc_luci_traffic_init(struct ubus_context *ctx) +{ + static const struct ubus_method methods[] = { UBUS_METHOD("get", get_rates, policy) }; + static struct ubus_object_type type = UBUS_OBJECT_TYPE("luci-client-rates", methods); + static struct ubus_object obj = { .name = "luci.client-rates", .type = &type, + .methods = methods, .n_methods = 1 }; + if (!initialized) { + avl_init(&clients, address_cmp, false, NULL); + avl_init(&flows, flow_cmp, false, NULL); + avl_init(&hosts, mac_cmp, false, NULL); + started = now_ms(); + load_state(); + timer.cb = sample_timer; + initialized = true; + uloop_timeout_set(&timer, 1); + } + return ubus_add_object(ctx, &obj); +} diff --git a/rpcd-mod-luci/src/webprobe.c b/rpcd-mod-luci/src/webprobe.c new file mode 100644 index 000000000..a3b011227 --- /dev/null +++ b/rpcd-mod-luci/src/webprobe.c @@ -0,0 +1,260 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Bounded, asynchronous TCP reachability checks for current DHCPv4 leases. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WEB_MAX_CLIENTS 1024 +#define WEB_CONCURRENCY 2 +#define WEB_TIMEOUT_MS 750 +#define WEB_INTERVAL_MS 300000 +#define WEB_LEASE_REFRESH_MS 30000 + +static const uint16_t web_ports[] = { 80, 8080, 5666, 443, 4430, 5667 }; +#define WEB_PORT_COUNT (sizeof(web_ports) / sizeof(web_ports[0])) + +struct web_client { + struct avl_node avl; + struct in_addr address; + unsigned char mac[6]; + uint64_t next_scan, checked; + uint32_t generation; + unsigned int next_port, pending, open_ports, checking; + bool scanning, ready; +}; +struct web_job { + struct uloop_fd fd; + struct uloop_timeout timeout; + struct web_client *client; + unsigned int port; +}; +static struct avl_tree web_clients; +static struct web_job web_jobs[WEB_CONCURRENCY]; +static struct uloop_timeout web_inventory, web_pump; +static uint32_t web_generation; +static bool web_initialized; +static struct blob_buf web_result; + +int rpc_luci_webprobe_init(struct ubus_context *ctx); +void rpc_luci_dhcp4_clients(void (*add)(const struct in_addr *, const unsigned char *)); + +static uint64_t web_now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int web_compare(const void *a, const void *b, void *priv) +{ + return memcmp(a, b, sizeof(struct in_addr)); +} + +static void web_close(struct web_job *job) +{ + uloop_timeout_cancel(&job->timeout); + if (job->fd.fd >= 0) { + uloop_fd_delete(&job->fd); + close(job->fd.fd); + job->fd.fd = -1; + } + job->client = NULL; +} + +static void web_cancel_client(struct web_client *client) +{ + for (int i = 0; i < WEB_CONCURRENCY; i++) + if (web_jobs[i].client == client) + web_close(&web_jobs[i]); + client->pending = 0; + client->scanning = false; +} + +static void web_finish(struct web_job *job, bool open) +{ + struct web_client *client = job->client; + if (!client) return; + if (open) client->checking |= 1U << job->port; + client->pending--; + web_close(job); + if (client->next_port == WEB_PORT_COUNT && !client->pending) { + client->open_ports = client->checking; + client->ready = true; + client->scanning = false; + client->checked = web_now(); + client->next_scan = client->checked + WEB_INTERVAL_MS; + } + uloop_timeout_set(&web_pump, 250); +} + +static void web_timeout(struct uloop_timeout *timeout) +{ + struct web_job *job = container_of(timeout, struct web_job, timeout); + web_finish(job, false); +} + +static void web_connected(struct uloop_fd *fd, unsigned int events) +{ + struct web_job *job = container_of(fd, struct web_job, fd); + int error = 0; + socklen_t len = sizeof(error); + bool open = !getsockopt(fd->fd, SOL_SOCKET, SO_ERROR, &error, &len) && !error; + web_finish(job, open); +} + +static void web_start(struct web_job *job, struct web_client *client) +{ + job->client = client; + job->port = client->next_port++; + client->pending++; + job->fd.fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + job->fd.cb = web_connected; + job->timeout.cb = web_timeout; + if (job->fd.fd < 0) { web_finish(job, false); return; } + struct sockaddr_in remote = { .sin_family = AF_INET, + .sin_addr = client->address, .sin_port = htons(web_ports[job->port]) }; + int ret = connect(job->fd.fd, (void *)&remote, sizeof(remote)); + if (!ret) { web_finish(job, true); return; } + if (errno != EINPROGRESS || uloop_fd_add(&job->fd, ULOOP_WRITE) < 0) { + web_finish(job, false); + return; + } + uloop_timeout_set(&job->timeout, WEB_TIMEOUT_MS); +} + +static void web_run(struct uloop_timeout *timeout) +{ + uint64_t now = web_now(); + struct web_client *client; + for (int i = 0; i < WEB_CONCURRENCY; i++) { + if (web_jobs[i].client) continue; + struct web_client *selected = NULL; + avl_for_each_element(&web_clients, client, avl) { + if ((client->scanning && client->next_port >= WEB_PORT_COUNT) || + (!client->scanning && now < client->next_scan)) + continue; + if (!selected || client->next_scan < selected->next_scan) + selected = client; + } + if (selected) { + if (!selected->scanning) { + selected->scanning = true; + selected->next_port = selected->pending = selected->checking = 0; + } + web_start(&web_jobs[i], selected); + } + } + /* A completed connect wakes the queue. When idle, sleep until the next + * due client instead of polling the cache every second. */ + bool free_slot = false; + for (int i = 0; i < WEB_CONCURRENCY; i++) + if (!web_jobs[i].client) free_slot = true; + if (free_slot) { + uint64_t delay = WEB_INTERVAL_MS; + avl_for_each_element(&web_clients, client, avl) { + if (client->scanning && client->next_port >= WEB_PORT_COUNT) continue; + uint64_t due = client->scanning || client->next_scan <= now ? 250 : client->next_scan - now; + if (due < delay) delay = due; + } + uloop_timeout_set(&web_pump, delay < 250 ? 250 : delay); + } +} + +static void web_add_client(const struct in_addr *address, const unsigned char *mac) +{ + uint32_t ip = ntohl(address->s_addr); + struct web_client *client; + /* Targets come only from local lease files, never from RPC input. */ + if (!ip || (ip >> 24) == 127 || (ip >> 24) == 0 || ip >= 0xe0000000U) + return; + client = avl_find_element(&web_clients, address, client, avl); + if (!client) { + if (web_clients.count >= WEB_MAX_CLIENTS || !(client = calloc(1, sizeof(*client)))) + return; + client->address = *address; + client->avl.key = &client->address; + avl_insert(&web_clients, &client->avl); + } + if (memcmp(client->mac, mac, 6)) { + web_cancel_client(client); + memcpy(client->mac, mac, 6); + client->open_ports = client->checking = 0; + client->ready = false; + client->checked = client->next_scan = 0; + } + client->generation = web_generation; +} + +static void web_refresh(struct uloop_timeout *timeout) +{ + struct web_client *client, *next; + web_generation++; + rpc_luci_dhcp4_clients(web_add_client); + avl_for_each_element_safe(&web_clients, client, avl, next) { + if (client->generation != web_generation) { + web_cancel_client(client); + avl_delete(&web_clients, &client->avl); + free(client); + } + } + uloop_timeout_set(&web_pump, 250); + uloop_timeout_set(&web_inventory, WEB_LEASE_REFRESH_MS); +} + +static int web_get(struct ubus_context *ctx, struct ubus_object *object, + struct ubus_request_data *req, const char *method, struct blob_attr *msg) +{ + struct web_client *client; + uint64_t now = web_now(); + blob_buf_init(&web_result, 0); + blobmsg_add_u32(&web_result, "interval_ms", WEB_INTERVAL_MS); + void *clients = blobmsg_open_table(&web_result, "clients"); + avl_for_each_element(&web_clients, client, avl) { + char ip[INET_ADDRSTRLEN], mac[18]; + inet_ntop(AF_INET, &client->address, ip, sizeof(ip)); + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + client->mac[0], client->mac[1], client->mac[2], client->mac[3], client->mac[4], client->mac[5]); + void *entry = blobmsg_open_table(&web_result, ip); + blobmsg_add_string(&web_result, "mac", mac); + blobmsg_add_u8(&web_result, "ready", client->ready); + blobmsg_add_u32(&web_result, "age_ms", client->ready ? now - client->checked : 0); + void *ports = blobmsg_open_array(&web_result, "ports"); + if (client->ready && now - client->checked < 2 * WEB_INTERVAL_MS) + for (int i = 0; i < WEB_PORT_COUNT; i++) + if (client->open_ports & (1U << i)) + blobmsg_add_u32(&web_result, NULL, web_ports[i]); + blobmsg_close_array(&web_result, ports); + blobmsg_close_table(&web_result, entry); + } + blobmsg_close_table(&web_result, clients); + ubus_send_reply(ctx, req, web_result.head); + return 0; +} + +int rpc_luci_webprobe_init(struct ubus_context *ctx) +{ + static const struct ubus_method methods[] = { UBUS_METHOD_NOARG("get", web_get) }; + static struct ubus_object_type type = UBUS_OBJECT_TYPE("luci-client-web", methods); + static struct ubus_object object = { .name = "luci.client-web", .type = &type, + .methods = methods, .n_methods = 1 }; + int ret = ubus_add_object(ctx, &object); + if (!ret && !web_initialized) { + avl_init(&web_clients, web_compare, false, NULL); + for (int i = 0; i < WEB_CONCURRENCY; i++) web_jobs[i].fd.fd = -1; + web_inventory.cb = web_refresh; + web_pump.cb = web_run; + web_initialized = true; + uloop_timeout_set(&web_inventory, 5000); + } + return ret; +} diff --git a/rpcd-mod-luci/tests/README.md b/rpcd-mod-luci/tests/README.md new file mode 100644 index 000000000..7d40fc097 --- /dev/null +++ b/rpcd-mod-luci/tests/README.md @@ -0,0 +1,75 @@ +# Client connection rates and boot-session totals + +`luci.client-rates get` accepts up to 1024 IP addresses and returns cached upload +and download bytes/second for each address, plus cumulative upload + download +bytes keyed by MAC in `totals`. It reads `NETLINK_NETFILTER` connection +counters; it never installs packet-filter rules or resets kernel counters. + +On QSDK, ECM's NSS/SFE/PPE statistics synchronization updates these conntrack +counters. NSS IPv4/IPv6 synchronization is normally once per second. This code +samples at two-second intervals and computes a rolling average over the last +three valid samples (about six seconds), weighted by actual elapsed time. The +window grows gradually after the initial baseline; invalid samples clear it. +The homepage refreshes only the rate cells every two seconds, independently of +the slower lease/status polling. Sampling starts when rpcd loads the plugin and +continues without any browser requests. Every sample discovers IPv4/IPv6 +neighbours and current DHCP leases without reverse-DNS queries. Current leases +win over stale neighbour records. Totals are keyed by MAC and each connection +retains its original owners, so address reuse does not transfer old traffic. + +An atomic checkpoint of totals, address bindings and connection baselines is +written to `/tmp/luci-client-traffic.state` after each completed sample. This is +RAM-backed on OpenWrt, not flash. The boot ID and format are checked on restore; +a router reboot starts a new accounting session. rpcd reloads retain totals and +only add subsequent deltas for surviving connections. Short connections that +end during a collector outage cannot be recovered; `incomplete` reports such +interruptions or accounting/receive limits. This is best-effort monitoring, not +billing accounting. Installing on a running router establishes a fresh baseline; +it cannot reconstruct connections that ended before installation. + +The cache is bounded to 4096 MACs, 8192 addresses and 32768 connection records. +Unused addresses expire after a day, but MAC totals remain for this boot. +Disconnected/unsupported platforms retry accounting availability every ten +seconds. They do not enable sysctls or install kernel modules automatically. + +The first complete dump establishes a baseline. A second sample produces rates. +Original-source bytes are upload; reply-source bytes are download, including +DNAT. The reverse direction is assigned accordingly. Destroy notifications +capture final counters for short flows. A bounded per-flow cache prevents whole +connection lifetimes being counted again at every poll. Totals accumulate raw +byte deltas, never the displayed or averaged rates. Interrupted dumps, +receive overruns and limits invalidate the sample instead of fabricating a rate. + +The accounting sysctl must already be enabled and conntrack netlink available. +The reader does not change these settings. Unsupported or warming-up values are +shown as `-`. Inactive supported clients show zero. This is routed/conntracked +traffic, not a physical port meter: pure layer-2 switched traffic and traffic +which never appears in conntrack are outside its scope. Proxies terminating on +the router are measured on their client-side connections. Bytes reflect IP +accounting, not Ethernet wire overhead. The DHCP table combines known addresses +for the same MAC, including IPv6 addresses from host hints, and normalizes them +to avoid duplicates. DUID/IAID remain in the lease objects for static reservations. + +## Regression checks + +Run `node tests/traffic-ui.js` from this package directory. It exercises address +normalization, IPv4/IPv6 aggregation, direction, sorting keys and warming-up UI. + +Build `tests/traffic.c` with the OpenWrt target compiler, the staging include/lib +paths, and `-lubus -lubox`, then execute the binary on a compatible target. It +constructs real netlink attribute layouts for IPv4, IPv6, DNAT, counter resets, +short-lived flows, a delayed dump record after destroy, baseline/rate calculation +rolling-window expiry, unequal sampling intervals, invalid-window reset, exact +byte totals, checkpoint restore, IP reassignment and unattended sampling. Test programs are not included in the installed package. + +The integration check should compare conntrack byte deltas with ECM connection +statistics while the matching connection is accelerated, then check the real +homepage with traffic. Do not infer accelerated accounting solely from a package +build or a nonzero interface counter. + +`connections` contains the latest completed conntrack dump count per MAC, +combining IPv4/IPv6 and counting a flow only once when both ends have the same +MAC. It includes idle tracked entries and counts records even without byte +counters. It is a two-second snapshot, not a TCP ESTABLISHED-only count. +Failed or stale snapshots return no counts, rendered as `-`; a valid empty +snapshot returns zero. Counts are not restored from historical traffic state. diff --git a/rpcd-mod-luci/tests/traffic-ui.js b/rpcd-mod-luci/tests/traffic-ui.js new file mode 100644 index 000000000..1556bb7ac --- /dev/null +++ b/rpcd-mod-luci/tests/traffic-ui.js @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const luci = path.resolve(__dirname, '../../..'); +const baseclass = { extend: o => o }; +const validation = new Function('baseclass', fs.readFileSync(path.join(luci, 'modules/luci-base/htdocs/luci-static/resources/validation.js'), 'utf8'))(baseclass); +const source = fs.readFileSync(path.join(luci, 'modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js'), 'utf8'); +const L = { bind: (fn, ctx, ...args) => fn.bind(ctx, ...args), hasSystemFeature: () => false, resolveDefault: (p, fallback) => Promise.resolve(p).catch(() => fallback), toArray: x => x == null ? [] : Array.isArray(x) ? x : [x] }; +let cells = [], calls = 0, polls = [], reply; +const document = {querySelectorAll: selector => selector.startsWith('#status_leases') ? [] : cells}; +const poll = {add: (fn, interval) => polls.push({fn, interval})}; +const rpc = {declare: spec => addresses => { + assert.equal(spec.object, 'luci.client-rates'); + assert.deepEqual(addresses, ['192.168.0.2']); + calls++; + return reply; +}}; +const view = new Function('baseclass','rpc','L','_','validation','poll','document','E', source)(baseclass, rpc, L, s=>s, validation, poll, document, (tag, attrs, text) => ({tag,attrs,text})); +// Formatting is supplied by LuCI; check its numeric sorting key separately. +String.prototype.format = function(value) { return String(value) + ' B/s'; }; +const lease = {ipaddr:'192.168.0.2', macaddr:'aa:bb:cc:dd:ee:ff',ip6addrs:['2001:db8::2/128']}; +const hints = {hosts: {'AA:BB:CC:DD:EE:FF':{ipaddrs:['192.168.0.2'],ip6addrs:['2001:0db8:0:0:0:0:0:2']}}}; +assert.deepEqual(view.clientAddresses(lease,hints), ['192.168.0.2','2001:db8:0:0:0:0:0:2']); +const rates={rates:{'192.168.0.2':{ready:true,upload:100,download:200},'2001:db8:0:0:0:0:0:2':{ready:true,upload:300,download:400}}}; +assert.equal(view.renderRate(lease,hints,rates,'upload')[0],400); +assert.equal(view.renderRate(lease,hints,rates,'download')[0],600); +assert.equal(view.renderRate(lease,hints,{},'upload')[1],'-'); +assert.deepEqual(view.clientAddresses({ipaddr:'invalid'}, {hosts:{}}),[]); +const mac = 'AA:BB:CC:DD:EE:FF'; +const totals = {...rates, totals:{[mac]:123456}}; +assert.equal(view.rateValue(view.clientAddresses(lease,hints),totals,'total',mac)[0],123456); +assert.equal(view.rateValue(['192.168.0.2'],totals,'total',mac)[0],123456); +assert.equal(view.rateValue([],totals,'total',mac.toLowerCase())[0],123456); +assert.equal(view.rateValue([],{},'total',mac)[1],'-'); +assert.equal(view.rateValue([], {totals:{[mac]:0}}, 'total',mac)[0],0); +assert.deepEqual(view.rateValue([], {connections:{[mac]:12}}, 'connections', mac), [12,'12']); +assert.deepEqual(view.rateValue([], {connections:{[mac]:0}}, 'connections', mac), [0,'0']); +assert.deepEqual(view.rateValue([], {}, 'connections', mac), [-1,'-']); +assert.deepEqual(view.rateValue([], {connections:{[mac]:6}}, 'connections', mac, false), [0,'0']); +assert.deepEqual(view.rateValue([], {}, 'connections', mac, false), [0,'0']); +assert.deepEqual(view.rateValue([], {connections:{[mac]:6}}, 'connections', mac, true), [6,'6']); +const web = ports => ({clients:{'192.168.0.2':{mac, ready:true, ports}}}); +assert.equal(view.clientURL(lease, web([4430,443,8080,80])), 'http://192.168.0.2/'); +assert.equal(view.clientURL(lease, web([443,8080])), 'http://192.168.0.2:8080/'); +assert.equal(view.clientURL(lease, web([4430,443])), 'https://192.168.0.2/'); +assert.equal(view.clientURL(lease, web([4430])), 'https://192.168.0.2:4430/'); +assert.equal(view.clientURL(lease, web([5666,443,5667])), 'http://192.168.0.2:5666/'); +assert.equal(view.clientURL(lease, web([5667])), 'https://192.168.0.2:5667/'); +assert.equal(view.clientURL(lease, web([])), null); +assert.equal(view.clientURL(lease, web([22])), null); +assert.equal(view.clientURL({...lease,macaddr:'00:11:22:33:44:55'}, web([80])), null); +assert.equal(view.clientURL({...lease,ipaddr:'javascript:alert(1)'}, web([80])), null); +assert.equal(view.clientURL(lease, {}), null); +const fnosClient = {macaddr: lease.macaddr, activeAddresses:['192.168.0.3', lease.ipaddr, '2001:db8::2']}; +assert.equal(view.isFnosClient(fnosClient, web([5666])), true); +assert.equal(view.isFnosClient(fnosClient, web([5667])), true); +assert.equal(view.isFnosClient(fnosClient, web([80,443])), false); +assert.equal(view.isFnosClient(fnosClient, {}), false); +assert.equal(view.isFnosClient({...fnosClient, activeAddresses:[]}, web([5666])), false); +assert.equal(view.isFnosClient({...fnosClient, macaddr:'00:11:22:33:44:55'}, web([5666])), false); +assert.equal(view.isFnosClient(fnosClient, {clients:{[lease.ipaddr]:{mac,ready:false,ports:[5666]}}}), false); +const link = view.renderClientIP(lease, web([5666,443])); +assert.equal(link.tag, 'a'); +assert.equal(link.text, lease.ipaddr); +assert.equal(link.attrs.href, 'http://192.168.0.2:5666/'); +assert.equal(link.attrs.target, '_blank'); +assert.equal(link.attrs.style, 'text-decoration:underline'); +assert.equal(view.renderClientIP(lease, web([])), lease.ipaddr); +const arp = `IP address HW type Flags HW address Mask Device +192.168.0.2 0x1 0x2 aa:bb:cc:dd:ee:ff * br-lan +192.168.0.8 0x1 0x6 00:11:22:33:44:66 * br-lan +192.168.0.9 0x1 0x0 00:11:22:33:44:77 * br-lan +192.168.9.1 0x1 0x2 00:11:22:33:44:88 * eth1 +192.168.0.10 0x1 0x2 00:00:00:00:00:00 * br-lan +192.168.0.11 0x1 0x2 01:00:5e:00:00:01 * br-lan +invalid 0x1 0x2 00:11:22:33:44:99 * br-lan`; +const arpClients=view.arpLeases(arp); +assert.equal(arpClients.length,2); +assert.equal(arpClients[0].macaddr,mac); +assert.equal(view.arpLeases('').length,0); +assert.equal(view.mergeLeases(arpClients,[],{hosts:{}}).length,2); +assert.equal(view.mergeLeases([{macaddr:mac,ipaddr:'192.168.0.2'},...arpClients],[],{hosts:{}}).length,2); +// One row per known MAC, with all family addresses and original reservation IDs. +const v4 = [ + {macaddr:mac.toLowerCase(), hostname:'desktop', ipaddr:'192.168.0.2'}, + {macaddr:mac, hostname:'desktop', ipaddr:'192.168.0.3'}, + {macaddr:'00:11:22:33:44:55', hostname:'desktop', ipaddr:'192.168.0.4'} +]; +const v6 = [ + {macaddr:mac, hostname:'desktop', ip6addr:'2001:db8::2', ip6addrs:['2001:db8::2/128'], duid:'DUID1', iaid:'01', interface:'br-lan'}, + {macaddr:mac.toLowerCase(), ip6addrs:['2001:0db8:0:0:0:0:0:2/128','2001:db8::3/128'], duid:'DUID1', iaid:'01'}, + {hostname:'desktop', ip6addrs:['2001:db8::4/128'], duid:'opaque'}, + {hostname:'desktop', ip6addrs:['2001:db8::5/128'], duid:'another'} +]; +const input = JSON.stringify([v4,v6]); +const merged = view.mergeLeases(v4,v6,{hosts:{}}); +assert.equal(merged.length,4); // Equal names with unknown or different MACs stay separate. +assert.equal(merged[0].macaddr,mac); +assert.deepEqual(merged[0].ipaddrs,['192.168.0.2','192.168.0.3']); +assert.deepEqual(merged[0].ip6addrs,['2001:db8::2','2001:db8::3/128']); +assert.equal(merged[0].leases6.length,1); +assert.equal(merged[0].leases6[0].duid,'DUID1'); +assert.equal(merged[0].leases6[0].iaid,'01'); +assert.deepEqual(merged[0].interfaces,['br-lan']); +assert.equal(JSON.stringify([v4,v6]),input); +const combined = view.mergeLeases([v4[0]],[v6[0]],hints)[0]; +assert.deepEqual(view.clientAddresses(combined,hints),view.clientAddresses(lease,hints)); +assert.equal(view.rateValue(view.clientAddresses(combined,hints),totals,'download',mac)[0],600); +assert.equal(view.rateValue(view.clientAddresses(combined,hints),totals,'total',mac)[0],123456); +const inferred = view.mergeLeases([v4[0]],[{...v6[0],macaddr:null}],hints); +assert.equal(inferred.length,1); +assert.equal(inferred[0].leases6[0].macaddr,mac); +const conflicting = {hosts:{...hints.hosts,'00:11:22:33:44:55':hints.hosts[mac]}}; +assert.equal(view.mergeLeases([v4[0]],[{...v6[0],macaddr:null}],conflicting).length,2); +assert.equal(view.mergeLeases([],[{macaddr:'00:00:00:00:00:00'},{}],{hosts:{}}).length,2); +assert.deepEqual(view.mergeLeases([],[],{hosts:{}}),[]); +const archived = view.mergeLeases([{...v4[0],_historical:true}], [{...v6[0],_historical:true}], hints)[0]; +assert.equal(archived.ipaddrs[0],'192.168.0.2'); +assert.equal(archived.ip6addrs[0],'2001:db8::2'); +assert.equal(archived.activeAddresses.length,0); +assert.equal(archived.leases.length,0); +assert.equal(view.clientAddresses(archived,hints).length,0); +assert.equal(view.rateValue([],totals,'total',mac)[0],123456); +const returned = view.mergeLeases([v4[0],{...v4[0],_historical:true}],[],hints); +assert.equal(returned.length,1); +assert.equal(returned[0].leases.length,1); +assert.equal(returned[0].ipaddrs.length,1); +const unknownHistory = view.mergeLeases([], [v6[2], {...v6[2],_historical:true}], {hosts:{}}); +assert.equal(unknownHistory.length,1); +assert.equal(unknownHistory[0].leases6.length,1); +const old4=view.handleCreateStaticLease, old6=view.handleCreateStaticLease6; +view.handleCreateStaticLease=record=>record; +view.handleCreateStaticLease6=record=>record; +view.isDUIDIAIDStatic={'duid1%01':true}; +const actions=view.leaseActions(combined).text; +assert.equal(actions.length,2); +assert.equal(actions[0].attrs.click().ipaddr,'192.168.0.2'); +assert.equal(actions[1].attrs.click().duid,'DUID1'); +assert.equal(actions[1].attrs.disabled,true); +view.handleCreateStaticLease=old4; view.handleCreateStaticLease6=old6; +console.log('traffic UI: MAC lease merging, unknown/conflicting identities and reservation actions passed'); +console.log('traffic UI: address normalization, no double-counting, directions and warm-up passed'); + +(async () => { + view.render([{}, {}]); + view.render([{}, {}]); + assert.equal(polls.length, 1); + assert.equal(polls[0].interval, 2); + await view.refreshRates(); + assert.equal(calls, 0); // No mounted table means no background requests. + function cell(direction) { + return {dataset:{addresses:'["192.168.0.2"]', direction}, + closest() {return {setAttribute: (name, value) => {this.sort = value;}};}}; + } + cells = [cell('upload'), cell('download')]; + reply = new Promise(resolve => {global.resolveRates = resolve;}); + const pending = view.refreshRates(); + // Simulate the normal status poll replacing the table during the RPC. + const oldCells = cells; + cells = [cell('upload'), cell('download')]; + global.resolveRates({rates:{'192.168.0.2':{ready:true,upload:100,download:200}}}); + await pending; + assert.equal(calls, 1); + assert.equal(cells[0].textContent, '100 B/s'); + assert.equal(cells[1].sort, 200); + assert.equal(oldCells[0].textContent, undefined); + reply = Promise.reject(new Error('unavailable')); + await view.refreshRates(); + assert.equal(cells[0].textContent, '-'); + assert.equal(cells[0].sort, -1); + console.log('traffic UI: independent polling, deduplication, row replacement and error fallback passed'); +})().catch(e => {console.error(e); process.exitCode = 1;}); diff --git a/rpcd-mod-luci/tests/traffic.c b/rpcd-mod-luci/tests/traffic.c new file mode 100644 index 000000000..ef7d248b6 --- /dev/null +++ b/rpcd-mod-luci/tests/traffic.c @@ -0,0 +1,207 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include +#define STATE_FILE "/tmp/luci-client-traffic.test.state" +#include "../src/traffic.c" + +void rpc_luci_traffic_discover(void (*add)(int, const void *, const unsigned char *)) {} + +static void put(struct nlmsghdr *msg, int type, const void *data, size_t len) +{ + struct nlattr *a = (void *)((char *)msg + NLMSG_ALIGN(msg->nlmsg_len)); + a->nla_type = type; + a->nla_len = NLA_HDRLEN + len; + memcpy((char *)a + NLA_HDRLEN, data, len); + msg->nlmsg_len = NLMSG_ALIGN(msg->nlmsg_len) + NLA_ALIGN(a->nla_len); +} + +static struct nlattr *begin(struct nlmsghdr *msg, int type) +{ + struct nlattr *a = (void *)((char *)msg + NLMSG_ALIGN(msg->nlmsg_len)); + a->nla_type = type | NLA_F_NESTED; + a->nla_len = NLA_HDRLEN; + msg->nlmsg_len += NLA_HDRLEN; + return a; +} + +static void end(struct nlmsghdr *msg, struct nlattr *a) +{ + a->nla_len = (char *)msg + msg->nlmsg_len - (char *)a; +} + +static void tuple(struct nlmsghdr *msg, int type, const char *src, const char *dst) +{ + struct address a; + struct nlattr *t = begin(msg, type), *ip = begin(msg, CTA_TUPLE_IP); + parse_address(src, &a); + put(msg, a.family == AF_INET ? CTA_IP_V4_SRC : CTA_IP_V6_SRC, a.bytes, a.family == AF_INET ? 4 : 16); + parse_address(dst, &a); + put(msg, a.family == AF_INET ? CTA_IP_V4_DST : CTA_IP_V6_DST, a.bytes, a.family == AF_INET ? 4 : 16); + end(msg, ip); + struct nlattr *proto = begin(msg, CTA_TUPLE_PROTO); + uint8_t num = 6; + uint16_t port = htons(1234); + put(msg, CTA_PROTO_NUM, &num, 1); + put(msg, CTA_PROTO_SRC_PORT, &port, 2); + put(msg, CTA_PROTO_DST_PORT, &port, 2); + end(msg, proto); + end(msg, t); +} + +static void counters(struct nlmsghdr *msg, int type, uint64_t bytes) +{ + unsigned char b[8]; + for (int i = 7; i >= 0; i--) { b[i] = bytes & 255; bytes >>= 8; } + struct nlattr *a = begin(msg, type); + put(msg, CTA_COUNTERS_BYTES, b, 8); + end(msg, a); +} + +static void feed(int family, const char *src, const char *dst, const char *reply_src, + uint32_t id, uint64_t up, uint64_t down, bool dead) +{ + char buffer[2048] = {0}; + struct nlmsghdr *msg = (void *)buffer; + struct nfgenmsg *nf = NLMSG_DATA(msg); + msg->nlmsg_len = NLMSG_LENGTH(sizeof(*nf)); + msg->nlmsg_type = (NFNL_SUBSYS_CTNETLINK << 8) | (dead ? IPCTNL_MSG_CT_DELETE : IPCTNL_MSG_CT_NEW); + nf->nfgen_family = family; + tuple(msg, CTA_TUPLE_ORIG, src, dst); + tuple(msg, CTA_TUPLE_REPLY, reply_src, src); + put(msg, CTA_ID, &id, 4); + counters(msg, CTA_COUNTERS_ORIG, up); + counters(msg, CTA_COUNTERS_REPLY, down); + assert(process_flow(msg)); +} + +static void count_record(int family, const char *src, const char *dst, const char *reply_src) +{ + char buffer[2048] = {0}; + struct nlmsghdr *msg = (void *)buffer; + struct nfgenmsg *nf = NLMSG_DATA(msg); + msg->nlmsg_len = NLMSG_LENGTH(sizeof(*nf)); + nf->nfgen_family = family; + tuple(msg, CTA_TUPLE_ORIG, src, dst); + tuple(msg, CTA_TUPLE_REPLY, reply_src, src); + /* Missing byte counters must not prevent connection counting. */ + count_connection(msg); +} + +static struct client *client(const char *ip) +{ + struct client *c = calloc(1, sizeof(*c)); + assert(parse_address(ip, &c->address)); + c->mac[0] = 2; + c->mac[5] = c->address.bytes[c->address.family == AF_INET ? 3 : 15]; + get_host(c->mac, true); + c->avl.key = &c->address; + avl_insert(&clients, &c->avl); + return c; +} + +int main(void) +{ + avl_init(&clients, address_cmp, false, NULL); + avl_init(&flows, flow_cmp, false, NULL); + avl_init(&hosts, mac_cmp, false, NULL); + started = now_ms(); + struct client *a = client("192.168.0.100"), *b = client("192.168.0.101"), *v6 = client("fd00::2"); + unsigned char v6mac[6]; + memcpy(v6mac, v6->mac, 6); + memcpy(v6->mac, a->mac, 6); + count_record(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8"); + count_record(AF_INET6, "fd00::2", "2001:db8::1", "2001:db8::1"); + count_record(AF_INET, "8.8.8.8", "203.0.113.1", "192.168.0.101"); + count_record(AF_INET, "192.168.0.100", "203.0.113.1", "192.168.0.100"); + assert(get_host(a->mac, false)->pending_connections == 3); + assert(get_host(b->mac, false)->pending_connections == 1); + assert(get_host(a->mac, false)->connections == 0); + get_host(a->mac, false)->pending_connections = 0; + get_host(b->mac, false)->pending_connections = 0; + memcpy(v6->mac, v6mac, 6); + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 1000, 2000, false); + assert(a->bytes[0] == 0 && a->bytes[1] == 0); /* First sample is a baseline. */ + finish_dump(); + assert(a->warm && !a->ready); + assert(get_host(a->mac, false)->total[0] == 0); + generation++; + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 3000, 7000, false); + assert(a->bytes[0] == 2000 && a->bytes[1] == 5000); + /* DNAT: original packets are downloads to reply.src, replies are uploads. */ + feed(AF_INET, "8.8.4.4", "203.0.113.1", "192.168.0.101", 2, 10000, 100, false); + assert(b->bytes[0] == 100 && b->bytes[1] == 10000); + feed(AF_INET6, "fd00::2", "2001:db8::1", "2001:db8::1", 3, 500, 1000, false); + assert(v6->bytes[0] == 500 && v6->bytes[1] == 1000); + /* Destroy notifications include the final bytes of short-lived flows. */ + feed(AF_INET, "192.168.0.100", "1.1.1.1", "1.1.1.1", 4, 80, 200, true); + assert(a->bytes[0] == 2080 && a->bytes[1] == 5200); + feed(AF_INET, "192.168.0.100", "1.1.1.1", "1.1.1.1", 4, 80, 200, false); + assert(a->bytes[0] == 2080 && a->bytes[1] == 5200); /* Late dump after destroy. */ + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 20, 30, false); + assert(a->bytes[0] == 2080 && a->bytes[1] == 5200); /* Reset cannot underflow. */ + sampled = now_ms() - 2000; + finish_dump(); + assert(a->ready && a->rate[0] >= 1035 && a->rate[0] <= 1040); + assert(a->rate[1] >= 2590 && a->rate[1] <= 2600); + /* A burst must stay in the rolling window, even across two quiet samples. */ + struct client rolling = { .ready = true, .bytes = {6000, 12000} }; + update_rate(&rolling, 2000); + assert(rolling.rate[0] == 3000 && rolling.rate[1] == 6000); + update_rate(&rolling, 2000); + update_rate(&rolling, 2000); + assert(rolling.rate[0] == 1000 && rolling.rate[1] == 2000); + update_rate(&rolling, 2000); + assert(rolling.rate[0] == 0 && rolling.rate[1] == 0); + /* Unequal intervals use sum(bytes) / sum(time), not mean(rate). */ + rolling.bytes[0] = 12000; + update_rate(&rolling, 4000); + assert(rolling.rate[0] == 1500); + rolling.ready = false; + update_rate(&rolling, 2000); + assert(rolling.rate[0] == 0 && rolling.duration[0] == 0); + rolling.ready = true; + rolling.bytes[0] = 2000; + update_rate(&rolling, 2000); + assert(rolling.rate[0] == 1000); + + /* Total bytes use exact deltas, never the rolling average. */ + struct host *ha = get_host(a->mac, false); + assert(ha->total[0] == 2080 && ha->total[1] == 5200); + uint64_t before = ha->total[0] + ha->total[1]; + save_state(); + struct client *c, *cn; + struct host *h, *hn; + clear_flows(); + avl_for_each_element_safe(&clients, c, avl, cn) { avl_delete(&clients, &c->avl); free(c); } + avl_for_each_element_safe(&hosts, h, avl, hn) { avl_delete(&hosts, &h->avl); free(h); } + load_state(); + struct address address; + parse_address("192.168.0.100", &address); + a = avl_find_element(&clients, &address, a, avl); + assert(a && !a->warm); + ha = get_host(a->mac, false); + assert(ha->total[0] + ha->total[1] == before); + /* Restored baselines only count the increment across an rpcd reload. */ + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 120, 230, false); + assert(ha->total[0] + ha->total[1] == before + 300); + /* An IP reassignment must not transfer the old connection to a new MAC. */ + unsigned char newmac[6] = {2, 1, 2, 3, 4, 5}; + bind_client(AF_INET, address.bytes, newmac); + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 220, 330, false); + assert(ha->total[0] + ha->total[1] == before + 500); + assert(get_host(newmac, false)->total[0] == 0); + /* Recovery preserves totals and the flow baselines, preventing replay. */ + reset_sampler("test_overrun"); + assert(!a->ready && !a->warm && flows.count > 0); + feed(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", 1, 220, 330, false); + assert(ha->total[0] + ha->total[1] == before + 500); + /* Collection is scheduled even when no browser has ever requested data. */ + uloop_init(); + sample_timer(&timer); + assert(timer.pending); + uloop_timeout_cancel(&timer); + reset_sampler("test_end"); + uloop_done(); + unlink(STATE_FILE); + puts("traffic: NAT, IPv6, rolling rates, exact totals, restart recovery, IP reassignment and continuous sampling passed"); + return 0; +} diff --git a/rpcd-mod-luci/tests/webprobe.c b/rpcd-mod-luci/tests/webprobe.c new file mode 100644 index 000000000..8bc4fbe19 --- /dev/null +++ b/rpcd-mod-luci/tests/webprobe.c @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +#include +#include "../src/webprobe.c" + +void rpc_luci_dhcp4_clients(void (*add)(const struct in_addr *, const unsigned char *)) {} + +static struct web_client *add(const char *ip, const unsigned char *mac) +{ + struct in_addr address; + struct web_client *client; + assert(inet_pton(AF_INET, ip, &address) == 1); + web_add_client(&address, mac); + return avl_find_element(&web_clients, &address, client, avl); +} + +int main(void) +{ + unsigned char mac[6] = {2, 1, 2, 3, 4, 5}, other[6] = {2, 6, 7, 8, 9, 10}; + uloop_init(); + avl_init(&web_clients, web_compare, false, NULL); + for (int i = 0; i < WEB_CONCURRENCY; i++) web_jobs[i].fd.fd = -1; + web_pump.cb = web_run; + web_inventory.cb = web_refresh; + assert(!add("127.0.0.1", mac)); + assert(!add("0.0.0.0", mac)); + assert(!add("224.0.0.1", mac)); + struct web_client *c = add("192.0.2.1", mac); + assert(c && web_clients.count == 1 && !c->ready); + assert(add("192.0.2.1", mac) == c && web_clients.count == 1); + /* Publish only a complete round, with exact port-to-bit correspondence. */ + c->scanning = true; + c->next_port = c->pending = WEB_PORT_COUNT; + for (int i = 0; i < WEB_PORT_COUNT; i++) { + web_jobs[0].client = c; + web_jobs[0].port = i; + web_finish(&web_jobs[0], i == 0 || i == 5); + assert(c->ready == (i == WEB_PORT_COUNT - 1)); + } + assert(c->open_ports == 33 && c->next_scan > web_now()); + /* Refresh failure must remove links which succeeded in a previous round. */ + c->scanning = true; c->checking = 0; c->pending = 1; + web_jobs[0].client = c; + web_jobs[0].timeout.cb = web_timeout; + web_timeout(&web_jobs[0].timeout); + assert(c->open_ports == 0 && c->ready); + /* IP reassignment invalidates cached results and closes pending sockets. */ + c->open_ports = 15; + web_jobs[0].client = c; + web_jobs[0].fd.fd = socket(AF_INET, SOCK_STREAM, 0); + int fd = web_jobs[0].fd.fd; + add("192.0.2.1", other); + assert(!c->ready && !c->open_ports && !web_jobs[0].client); + assert(close(fd) == -1 && errno == EBADF); + /* A lease disappearing removes its cached entry and pending jobs. */ + web_refresh(&web_inventory); + assert(!web_clients.count); + uloop_timeout_cancel(&web_inventory); + uloop_timeout_cancel(&web_pump); + uloop_done(); + puts("webprobe: bounded targets, round publication, timeout, IP reassignment and lease removal passed"); + return 0; +} diff --git a/rpcd-mod-luci/tests/webprobe.md b/rpcd-mod-luci/tests/webprobe.md new file mode 100644 index 000000000..a0d6eabf7 --- /dev/null +++ b/rpcd-mod-luci/tests/webprobe.md @@ -0,0 +1,36 @@ +# DHCP client web links + +The `luci.client-web get` RPC returns cached TCP reachability for current DHCPv4 +leases, keyed by IPv4 address and tagged with the client's MAC. It accepts no +scan targets or ports from the caller. No scanning is triggered by reading it. + +The collector starts five seconds after rpcd loads, refreshes the lease inventory +every 30 seconds, and checks each client again five minutes after its previous +round completes. At most two nonblocking TCP connects run concurrently, with +a 750 ms timeout each and a 250 ms delay between queue refills. Idle queues +sleep until the next scheduled probe. Earliest-due clients are scheduled first so large lease +lists cannot starve later addresses. The cache holds at most 1024 clients, is +kept in memory only, and is cleared on lease removal or MAC reassignment. Results +are published after the entire round completes; links from unsuccessful rounds +are removed and results older than ten minutes are not exposed as open ports. + +The frontend uses exactly one link, in this order: + +- HTTP: 80, 8080, 5666 +- HTTPS: 443, 4430, 5667 + +Default ports are omitted from URLs. Links display only the underlined IP address, with the full URL in the link target +and open in a new tab with `noopener noreferrer`. Unknown or unavailable clients +remain plain text. Results with a different MAC from the displayed lease are +ignored. The regular overview refresh reads the cache without blocking on scans. + +An open TCP port is a reachability result, not an HTTP response or TLS certificate +validation. The protocol is selected by the fixed port mapping above. No HTTP +requests or TLS handshakes are sent by the scanner. + +Compile `webprobe.c` as a standalone target-native test with the staging headers +and libraries (`-lubus -lubox`). It covers target validation, deduplication, +complete-round publication, timeout results, MAC changes and lease removal. +`traffic-ui.js` also checks URL priority, schemes, ports and stale-MAC rejection. +On a live router, compare the cached port list with TCP reachability and verify +the single rendered IP link, URL, target and continued status polling.