Skip to content

client: identify persistent clients via IPv6 NDP neighbor table#8248

Open
AndyHazz wants to merge 1 commit intoAdguardTeam:masterfrom
AndyHazz:ndp-client-identification
Open

client: identify persistent clients via IPv6 NDP neighbor table#8248
AndyHazz wants to merge 1 commit intoAdguardTeam:masterfrom
AndyHazz:ndp-client-identification

Conversation

@AndyHazz
Copy link

@AndyHazz AndyHazz commented Feb 15, 2026

Summary

  • When a DNS query arrives from an IPv6 address, AGH currently cannot match it to a persistent client by MAC because MACByIP() only checks DHCPv4 leases
  • This adds an NDP (Neighbor Discovery Protocol) neighbor table lookup as a fallback in findByIP(), FindLoose(), and ApplyClientFiltering()
  • The kernel's NDP cache maps IPv6 addresses to MACs for all devices on the local network, so persistent clients configured with MAC identifiers are now correctly matched regardless of query IP version

Motivation

On home networks where AdGuard Home acts as the DNS server and DHCP server, per-client filtering rules (e.g. blocking YouTube on children's devices) rely on identifying clients by MAC address. When IPv6 is enabled, devices send DNS queries from auto-generated IPv6 addresses (SLAAC + privacy extensions) that have no corresponding DHCPv4 lease. These queries appear as unidentified clients, bypassing per-client rules.

The NDP neighbor table already contains the IPv6→MAC mapping (populated by the kernel when processing incoming packets), but AGH doesn't consult it. The existing arpdb infrastructure only reads /proc/net/arp (IPv4) and doesn't reach the ip neigh fallback that includes IPv6.

Implementation

  • Adds an NDP cache (map[netip.Addr]net.HardwareAddr) to client.Storage, populated by running ip -6 neigh
  • Cache is refreshed alongside the existing ARP refresh cycle via ReloadARP()
  • On-demand refresh (at most every 30s) when an unknown IPv6 address is encountered, so newly connected devices are identified promptly
  • NDP output parsing is extracted into parseNDPNeigh() for testability
  • NDP data source is injectable via StorageConfig.NDPData (defaults to exec, overridden in tests)
  • Lookup is added to findByIP(), FindLoose(), and ApplyClientFiltering() after the DHCP MAC lookup fails

Test plan

  • Tested on Raspberry Pi (armv7l) running AGH v0.107.71 as DNS+DHCP server
  • DNS query from laptop via IPv6 correctly identified as persistent client via NDP→MAC→client lookup
  • IPv4 queries continue to work as before (DHCP lease path unchanged)
  • Unit tests for parseNDPNeigh() — 8 cases covering typical output, missing lladdr, short lines, bad IP/MAC, router flag, empty input, mixed valid/invalid
  • Integration tests for Storage.Find() and Storage.FindLoose() — IPv6 client found via NDP, unknown IPv4/IPv6 not found, graceful handling when NDP command fails
  • Full internal/client test suite passes

🤖 Generated with Claude Code

@windsurf-bot
Copy link

windsurf-bot bot commented Feb 15, 2026

I ran into an unexpected issue while reviewing this PR. Please try again later.

@AndyHazz AndyHazz force-pushed the ndp-client-identification branch from e5f42d6 to 757928e Compare February 15, 2026 18:47
Currently, persistent clients configured with a MAC address can only be
identified when they query over IPv4 (via DHCP lease lookup).  When the
same device queries over IPv6, AGH cannot resolve the source IPv6
address back to a MAC address, so the client appears unidentified and
per-client filtering rules don't apply.

Add an NDP (Neighbor Discovery Protocol) cache to Storage that
periodically reads the kernel's IPv6 neighbor table (`ip -6 neigh`) and
maps IPv6 addresses to MAC addresses.  The cache is refreshed alongside
ARP updates and on-demand when an address is not found and the cache is
stale (>30s).

The NDP lookup is used as a fallback in findByIP, FindLoose, and
ApplyClientFiltering when the DHCP lease lookup returns no result.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@AndyHazz AndyHazz force-pushed the ndp-client-identification branch from 757928e to 26f2f8c Compare February 15, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant