mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 16:02:51 +08:00
🎈 Sync 2026-09-16 00:45:17
This commit is contained in:
1 parent
d5fe29717e
commit
afb665d9c0
10 files changed
+3785
No files matched your search
@@ -0,0 +1,55 @@
|
||||
#
|
||||
# Copyright (C) 2016-2017 Jo-Philipp Wich <jo@mein.io>
|
||||
#
|
||||
# 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 <jo@mein.io>
|
||||
|
||||
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))
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,781 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
/* Read-only conntrack accounting, including ECM/NSS synchronized counters. */
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/ether.h>
|
||||
#include <sys/socket.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <linux/netfilter/nfnetlink.h>
|
||||
#include <linux/netfilter/nfnetlink_conntrack.h>
|
||||
#include <libubox/avl.h>
|
||||
#include <libubox/uloop.h>
|
||||
#include <libubus.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
/* Bounded, asynchronous TCP reachability checks for current DHCPv4 leases. */
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <libubox/avl.h>
|
||||
#include <libubox/uloop.h>
|
||||
#include <libubus.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;});
|
||||
@@ -0,0 +1,207 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
#include <assert.h>
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
#include <assert.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user