diff --git a/docker/routlin-dash/app/pages/dhcpleases/view.py b/docker/routlin-dash/app/pages/dhcpleases/view.py index 49be1d3..e83e143 100644 --- a/docker/routlin-dash/app/pages/dhcpleases/view.py +++ b/docker/routlin-dash/app/pages/dhcpleases/view.py @@ -1,4 +1,5 @@ import ipaddress +import json import os import glob from datetime import datetime, timezone @@ -46,26 +47,10 @@ def _vendor_cell(vendor): def _get_arp_table(): - """Return {mac_lower: entry} from /proc/net/arp (host-mounted). ATF_COM (0x2) flag means - the entry is complete; entries without it (incomplete) are excluded.""" + """Return {mac_lower: entry} from the ARP cache written by maintenance.py.""" try: - entries = {} - with open('/host/proc/net/arp') as f: - next(f) # skip header line - for line in f: - parts = line.split() - if len(parts) < 6: - continue - ip = parts[0] - flags = int(parts[2], 16) - mac = parts[3].lower() - iface = parts[5] - if not (flags & 0x2): - continue - if mac == '00:00:00:00:00:00': - continue - entries[mac] = {'ip': ip, 'iface': iface, 'state': 'REACHABLE'} - return entries + with open('/var/lib/misc/arp-cache.json') as f: + return json.load(f) except Exception: return {} diff --git a/docker/routlin-dash/docker-compose.yml b/docker/routlin-dash/docker-compose.yml index b9ce78c..697ce77 100644 --- a/docker/routlin-dash/docker-compose.yml +++ b/docker/routlin-dash/docker-compose.yml @@ -14,7 +14,6 @@ services: - /sys/devices:/sys/devices:ro - /etc/localtime:/etc/localtime:ro - /var/lib/misc:/var/lib/misc:ro - - /proc/net/arp:/host/proc/net/arp:ro - /var/log/freeradius:/var/log/freeradius environment: - PYTHONPATH=/routlin_location diff --git a/routlin/maintenance.py b/routlin/maintenance.py index 41d9cb2..00e839d 100644 --- a/routlin/maintenance.py +++ b/routlin/maintenance.py @@ -38,6 +38,7 @@ CONFIG_FILE = SCRIPT_DIR / "config.json" CACHE_SERVICE_FILE = SCRIPT_DIR / ".ddns-last-service" LOG_FILE = SCRIPT_DIR / "ddns.log" RADIUS_LOG_FILE = Path("/var/log/freeradius/radius.log") +ARP_CACHE_FILE = Path("/var/lib/misc/arp-cache.json") # log is assigned in setup_logging() after config is loaded log = None @@ -534,6 +535,25 @@ def rotate_radius_log(radius_cfg): # Main # =================================================================== +def refresh_arp_cache(): + try: + result = subprocess.run(['ip', 'neigh'], capture_output=True, text=True, timeout=5) + entries = {} + for line in result.stdout.splitlines(): + parts = line.split() + if 'lladdr' not in parts: + continue + state = parts[-1] + if state in ('FAILED', 'PERMANENT', 'NOARP', 'INCOMPLETE'): + continue + idx = parts.index('lladdr') + mac = parts[idx + 1].lower() + entries[mac] = {'ip': parts[0], 'state': state} + ARP_CACHE_FILE.write_text(json.dumps(entries)) + except Exception as exc: + print(f"WARNING: Could not refresh ARP cache: {exc}") + + def run_update(cfg, force=False, getip_only=False): """Perform a single DDNS update pass. If force=True, bypasses the cached IP check and always updates. @@ -593,6 +613,7 @@ def main(): run_update(cfg, force=args.force) rotate_radius_log(cfg.get("_radius", {})) + refresh_arp_cache() if __name__ == "__main__": main()