mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 19:35:13 +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