Skip to content

Commit 934d66d

Browse files
committed
per client stats
1 parent 2d95212 commit 934d66d

File tree

7 files changed

+186
-8
lines changed

7 files changed

+186
-8
lines changed

components/cmd_router/cmd_router.c

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,26 +1283,42 @@ static int show(int argc, char **argv)
12831283
if (connect_count > 0) {
12841284
connected_client_t clients[8];
12851285
int count = get_connected_clients(clients, 8);
1286-
1286+
12871287
if (count > 0) {
1288+
// Fetch per-client traffic stats
1289+
client_stats_entry_t stats[CLIENT_STATS_MAX];
1290+
int stats_count = client_stats_get_all(stats, CLIENT_STATS_MAX);
1291+
12881292
printf("\nClient Details:\n");
1289-
printf("MAC Address IP Address Device Name\n");
1290-
printf("---------------- --------------- ------------------\n");
1291-
1293+
printf("MAC Address IP Address Device Name TX / RX\n");
1294+
printf("---------------- --------------- ------------------- ------------------\n");
1295+
12921296
for (int i = 0; i < count; i++) {
12931297
char mac_str[18];
12941298
sprintf(mac_str, "%02X:%02X:%02X:%02X:%02X:%02X",
12951299
clients[i].mac[0], clients[i].mac[1], clients[i].mac[2],
12961300
clients[i].mac[3], clients[i].mac[4], clients[i].mac[5]);
1297-
1301+
12981302
char ip_str[16] = "N/A";
12991303
if (clients[i].has_ip) {
13001304
ip4_addr_t addr;
13011305
addr.addr = clients[i].ip;
13021306
sprintf(ip_str, IPSTR, IP2STR(&addr));
13031307
}
1304-
1305-
printf("%-17s %-15s %s\n", mac_str, ip_str, clients[i].name);
1308+
1309+
// Find matching traffic stats by MAC
1310+
char traffic_str[32] = "-";
1311+
for (int j = 0; j < stats_count; j++) {
1312+
if (memcmp(stats[j].mac, clients[i].mac, 6) == 0) {
1313+
char tx_buf[12], rx_buf[12];
1314+
format_bytes_human(stats[j].bytes_sent, tx_buf, sizeof(tx_buf));
1315+
format_bytes_human(stats[j].bytes_received, rx_buf, sizeof(rx_buf));
1316+
snprintf(traffic_str, sizeof(traffic_str), "%s / %s", tx_buf, rx_buf);
1317+
break;
1318+
}
1319+
}
1320+
1321+
printf("%-17s %-15s %-19s %s\n", mac_str, ip_str, clients[i].name, traffic_str);
13061322
}
13071323
}
13081324
}

components/http_server/http_server.c

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,10 @@ static esp_err_t mappings_get_handler(httpd_req_t *req)
16331633
int client_count = get_connected_clients(clients, MAX_DISPLAYED_CLIENTS);
16341634
connect_count = client_count;
16351635

1636+
/* Fetch per-client traffic stats */
1637+
client_stats_entry_t stats[CLIENT_STATS_MAX];
1638+
int stats_count = client_stats_get_all(stats, CLIENT_STATS_MAX);
1639+
16361640
if (client_count > 0) {
16371641
for (int i = 0; i < client_count; i++) {
16381642
char ip_str[16] = "-";
@@ -1660,16 +1664,30 @@ static esp_err_t mappings_get_handler(httpd_req_t *req)
16601664
}
16611665
js_name[j] = '\0';
16621666

1667+
/* Find matching traffic stats by MAC */
1668+
char traffic_str[32] = "-";
1669+
for (int s = 0; s < stats_count; s++) {
1670+
if (memcmp(stats[s].mac, clients[i].mac, 6) == 0) {
1671+
char tx_buf[12], rx_buf[12];
1672+
format_bytes_human(stats[s].bytes_sent, tx_buf, sizeof(tx_buf));
1673+
format_bytes_human(stats[s].bytes_received, rx_buf, sizeof(rx_buf));
1674+
snprintf(traffic_str, sizeof(traffic_str), "%s / %s", tx_buf, rx_buf);
1675+
break;
1676+
}
1677+
}
1678+
16631679
snprintf(row, sizeof(row),
16641680
"<tr>"
16651681
"<td>%s</td>"
16661682
"<td>%s</td>"
16671683
"<td>%s</td>"
1684+
"<td>%s</td>"
16681685
"<td><button type='button' class='green-button' onclick=\"fillDhcpForm('%s','%s','%s')\">Reserve</button></td>"
16691686
"</tr>",
16701687
mac_str,
16711688
ip_str,
16721689
clients[i].name[0] ? clients[i].name : "-",
1690+
traffic_str,
16731691
mac_str,
16741692
clients[i].has_ip ? ip_str : "",
16751693
js_name
@@ -1678,7 +1696,7 @@ static esp_err_t mappings_get_handler(httpd_req_t *req)
16781696
}
16791697
} else {
16801698
httpd_resp_send_chunk(req,
1681-
"<tr><td colspan='4' style='text-align:center; color:#888;'>No clients connected</td></tr>",
1699+
"<tr><td colspan='5' style='text-align:center; color:#888;'>No clients connected</td></tr>",
16821700
HTTPD_RESP_USE_STRLEN);
16831701
}
16841702

components/http_server/pages.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ document.getElementById('dhcp_mac').scrollIntoView({behavior: 'smooth', block: '
532532
<th>MAC Address</th>\
533533
<th>IP Address</th>\
534534
<th>Device Name</th>\
535+
<th>Traffic (TX / RX)</th>\
535536
<th>Action</th>\
536537
</tr>\
537538
</thead>\

include/client_stats.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* Per-client traffic statistics for AP-connected clients.
2+
*
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
#pragma once
6+
7+
#include <stdint.h>
8+
#include <string.h>
9+
#include "router_config.h"
10+
11+
#define CLIENT_STATS_MAX AP_MAX_CONNECTIONS
12+
13+
typedef struct {
14+
uint8_t mac[6];
15+
uint8_t active; /* 1 = slot in use (has stats) */
16+
uint8_t connected; /* 1 = currently connected to AP */
17+
uint64_t bytes_sent; /* bytes sent TO this client (linkoutput) */
18+
uint64_t bytes_received; /* bytes received FROM this client (input) */
19+
uint32_t packets_sent;
20+
uint32_t packets_received;
21+
} client_stats_entry_t;
22+
23+
/* Called from WiFi event handlers on client connect/disconnect */
24+
void client_stats_on_connect(const uint8_t *mac);
25+
void client_stats_on_disconnect(const uint8_t *mac);
26+
27+
/* Copy active entries into caller-provided buffer. Returns count copied. */
28+
int client_stats_get_all(client_stats_entry_t *out, int max_entries);
29+
30+
/* Zero all counters but keep active/mac state */
31+
void client_stats_reset_all(void);
32+
33+
/* Format byte count as human-readable string (e.g. "1.2 MB") */
34+
void format_bytes_human(uint64_t bytes, char *buf, size_t len);

include/router_globals.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#pragma once
1010

1111
#include "router_config.h"
12+
#include "client_stats.h"
1213
#include "dhcp_reservations.h"
1314
#include "portmap.h"
1415
#include "wifi_config.h"

main/esp32_nat_router.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ static void wifi_ap_event_handler(void* arg, esp_event_base_t event_base,
382382
} else if (event_id == WIFI_EVENT_AP_STACONNECTED) {
383383
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
384384
connect_count++;
385+
client_stats_on_connect(event->mac);
385386
const char* name = lookup_device_name_by_mac(event->mac);
386387
if (name) {
387388
ESP_LOGI(TAG, "Client connected: %02X:%02X:%02X:%02X:%02X:%02X (%s) - %d total",
@@ -397,6 +398,7 @@ static void wifi_ap_event_handler(void* arg, esp_event_base_t event_base,
397398
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
398399
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
399400
connect_count--;
401+
client_stats_on_disconnect(event->mac);
400402
const char* name = lookup_device_name_by_mac(event->mac);
401403
if (name) {
402404
ESP_LOGI(TAG, "Client disconnected: %02X:%02X:%02X:%02X:%02X:%02X (%s) - %d remain",
@@ -471,6 +473,7 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
471473
{
472474
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
473475
connect_count++;
476+
client_stats_on_connect(event->mac);
474477

475478
/* Look up device name from DHCP reservations */
476479
const char* name = lookup_device_name_by_mac(event->mac);
@@ -490,6 +493,7 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base,
490493
{
491494
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
492495
connect_count--;
496+
client_stats_on_disconnect(event->mac);
493497

494498
/* Look up device name from DHCP reservations */
495499
const char* name = lookup_device_name_by_mac(event->mac);

main/netif_hooks.c

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* and AP interfaces to intercept packets for filtering and monitoring.
66
*/
77

8+
#include <inttypes.h>
89
#include <string.h>
910
#include <time.h>
1011
#include "esp_log.h"
@@ -18,6 +19,7 @@
1819
#include "lwip/prot/ip4.h"
1920
#include "lwip/inet_chksum.h"
2021
#include "acl.h"
22+
#include "client_stats.h"
2123
#include "pcap_capture.h"
2224
#include "router_config.h"
2325
#include "wifi_config.h"
@@ -43,6 +45,88 @@ static netif_input_fn original_ap_netif_input = NULL;
4345
static netif_linkoutput_fn original_ap_netif_linkoutput = NULL;
4446
static struct netif *ap_netif = NULL;
4547

48+
// Per-client traffic statistics for AP clients
49+
static client_stats_entry_t client_stats[CLIENT_STATS_MAX];
50+
51+
static inline client_stats_entry_t* find_client_stats(const uint8_t *mac) {
52+
for (int i = 0; i < CLIENT_STATS_MAX; i++) {
53+
if (client_stats[i].active && memcmp(client_stats[i].mac, mac, 6) == 0) {
54+
return &client_stats[i];
55+
}
56+
}
57+
return NULL;
58+
}
59+
60+
void client_stats_on_connect(const uint8_t *mac) {
61+
// Keep existing stats on reconnect
62+
client_stats_entry_t *existing = find_client_stats(mac);
63+
if (existing) {
64+
existing->connected = 1;
65+
return;
66+
}
67+
// Find free slot: prefer inactive, then disconnected
68+
int free_slot = -1;
69+
int disconnected_slot = -1;
70+
for (int i = 0; i < CLIENT_STATS_MAX; i++) {
71+
if (!client_stats[i].active) {
72+
free_slot = i;
73+
break;
74+
} else if (!client_stats[i].connected && disconnected_slot < 0) {
75+
disconnected_slot = i;
76+
}
77+
}
78+
int slot = (free_slot >= 0) ? free_slot : disconnected_slot;
79+
if (slot >= 0) {
80+
memcpy(client_stats[slot].mac, mac, 6);
81+
client_stats[slot].bytes_sent = 0;
82+
client_stats[slot].bytes_received = 0;
83+
client_stats[slot].packets_sent = 0;
84+
client_stats[slot].packets_received = 0;
85+
client_stats[slot].active = 1;
86+
client_stats[slot].connected = 1;
87+
}
88+
}
89+
90+
void client_stats_on_disconnect(const uint8_t *mac) {
91+
client_stats_entry_t *entry = find_client_stats(mac);
92+
if (entry) {
93+
entry->connected = 0;
94+
}
95+
}
96+
97+
int client_stats_get_all(client_stats_entry_t *out, int max_entries) {
98+
int count = 0;
99+
for (int i = 0; i < CLIENT_STATS_MAX && count < max_entries; i++) {
100+
if (client_stats[i].active) {
101+
memcpy(&out[count], &client_stats[i], sizeof(client_stats_entry_t));
102+
count++;
103+
}
104+
}
105+
return count;
106+
}
107+
108+
void client_stats_reset_all(void) {
109+
for (int i = 0; i < CLIENT_STATS_MAX; i++) {
110+
if (client_stats[i].active) {
111+
client_stats[i].bytes_sent = 0;
112+
client_stats[i].bytes_received = 0;
113+
client_stats[i].packets_sent = 0;
114+
client_stats[i].packets_received = 0;
115+
}
116+
}
117+
}
118+
119+
void format_bytes_human(uint64_t bytes, char *buf, size_t len) {
120+
if (bytes >= 1073741824ULL)
121+
snprintf(buf, len, "%.1f GB", (double)bytes / 1073741824.0);
122+
else if (bytes >= 1048576ULL)
123+
snprintf(buf, len, "%.1f MB", (double)bytes / 1048576.0);
124+
else if (bytes >= 1024ULL)
125+
snprintf(buf, len, "%.1f KB", (double)bytes / 1024.0);
126+
else
127+
snprintf(buf, len, "%" PRIu64 " B", bytes);
128+
}
129+
46130
// Hook function to count received bytes via netif input and ACL check
47131
static err_t netif_input_hook(struct pbuf *p, struct netif *netif) {
48132
bool is_acl_monitored = false;
@@ -456,6 +540,16 @@ static err_t ap_netif_input_hook(struct pbuf *p, struct netif *netif) {
456540
// Clamp TCP MSS on SYN packets from clients
457541
clamp_tcp_mss(p, ap_mss_clamp);
458542

543+
// Per-client byte counting: source MAC = client
544+
if (p != NULL && p->len >= 14) {
545+
const uint8_t *src_mac = ((const uint8_t *)p->payload) + 6;
546+
client_stats_entry_t *entry = find_client_stats(src_mac);
547+
if (entry) {
548+
entry->bytes_received += p->tot_len;
549+
entry->packets_received++;
550+
}
551+
}
552+
459553
// Capture packet based on mode and ACL monitor flag (AP interface = true)
460554
if (pcap_should_capture(is_acl_monitored, true)) {
461555
pcap_capture_packet(p);
@@ -492,6 +586,16 @@ static err_t ap_netif_linkoutput_hook(struct netif *netif, struct pbuf *p) {
492586
// Clamp TCP MSS on SYN/SYN-ACK packets to clients
493587
clamp_tcp_mss(p, ap_mss_clamp);
494588

589+
// Per-client byte counting: dest MAC = client
590+
if (p != NULL && p->len >= 14) {
591+
const uint8_t *dst_mac = (const uint8_t *)p->payload;
592+
client_stats_entry_t *entry = find_client_stats(dst_mac);
593+
if (entry) {
594+
entry->bytes_sent += p->tot_len;
595+
entry->packets_sent++;
596+
}
597+
}
598+
495599
// Capture packet based on mode and ACL monitor flag (AP interface = true)
496600
if (pcap_should_capture(is_acl_monitored, true)) {
497601
pcap_capture_packet(p);

0 commit comments

Comments
 (0)