router-ui: per-device traffic, last seen, and saner device saves
- nftables dynamic sets count bytes per LAN address; a 1-min tick folds them into totals that survive a ruleset reload - devices page shows last seen + per-device down/up - saving no longer pushes every DHCP lease into devices.toml, only rows with a reservation, note or block - empty-state text on the traffic graphs instead of blank space Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
355e696c43
commit
c00a1f85bc
4 changed files with 336 additions and 39 deletions
|
|
@ -170,6 +170,33 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
# Snapshot the nftables per-device counters and fold them into totals that
|
||||
# outlive a ruleset reload. Root, because reading an nft set needs
|
||||
# NET_ADMIN; the web app only ever reads the resulting file.
|
||||
systemd.services.router-accounting = {
|
||||
description = "Accumulate per-device traffic counters";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pythonEnv}/bin/python3 ${../scripts/router-ui.py} tick";
|
||||
# No StateDirectory: this unit is root, and StateDirectory would chown
|
||||
# /var/lib/router-ui away from the router-ui user on every tick. The
|
||||
# script mkdir -p's it, so a cold start before the web app is fine.
|
||||
};
|
||||
environment = {
|
||||
ROUTER_UI_STATE = stateDir;
|
||||
DNSMASQ_LEASES = "/var/lib/dnsmasq/dnsmasq.leases";
|
||||
NFT_BIN = "${pkgs.nftables}/bin/nft";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.router-accounting = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "2min";
|
||||
OnUnitActiveSec = "1min";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.router-speedtest = {
|
||||
description = "Record a speedtest result";
|
||||
serviceConfig = {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,44 @@ in
|
|||
}
|
||||
'';
|
||||
};
|
||||
# Per-device byte counters for the router UI's traffic page.
|
||||
#
|
||||
# Dynamic sets keyed on the LAN address, so hosts appear on their own —
|
||||
# nothing here needs to know which devices exist. prerouting/postrouting
|
||||
# rather than forward, because forward misses LAN↔router traffic, and on
|
||||
# this box that includes every Jellyfin stream. Priority -300 puts both
|
||||
# chains ahead of NAT, so addresses are still the device's own.
|
||||
#
|
||||
# Counters reset whenever this table reloads (i.e. every switch that
|
||||
# touches nftables). router-accounting.service snapshots them each
|
||||
# minute and accumulates deltas into a file that survives, so the UI's
|
||||
# totals are continuous even though these counters aren't.
|
||||
tables.accounting = {
|
||||
family = "ip";
|
||||
content = ''
|
||||
set up {
|
||||
type ipv4_addr
|
||||
flags dynamic
|
||||
counter
|
||||
timeout 7d
|
||||
}
|
||||
set down {
|
||||
type ipv4_addr
|
||||
flags dynamic
|
||||
counter
|
||||
timeout 7d
|
||||
}
|
||||
chain pre {
|
||||
type filter hook prerouting priority -300; policy accept;
|
||||
iifname "eth0" update @up { ip saddr }
|
||||
}
|
||||
chain post {
|
||||
type filter hook postrouting priority -300; policy accept;
|
||||
oifname "eth0" update @down { ip daddr }
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
# Use a distinct table name so we don't share `ip nat` with Docker —
|
||||
# Docker manages its own DOCKER/PREROUTING chains in `ip nat`, and
|
||||
# NixOS's nftables module rebuilds whichever tables it owns on every
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue