From 4efb3994e140c71cf0a5fd7988101a3382dce15b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jun 2026 00:48:33 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=84=20Sync=202026-06-03=2000:48:32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hickory-dns/Makefile | 2 +- hickory-dns/files/etc/hickory-dns/0.0.10.zone | 8 - hickory-dns/files/etc/hickory-dns/0.zone | 8 - .../files/etc/hickory-dns/127.0.0.1.zone | 10 - hickory-dns/files/etc/hickory-dns/255.zone | 8 - .../files/etc/hickory-dns/3.168.192.zone | 8 - .../files/etc/hickory-dns/d.f.ip6.arpa.zone | 8 - .../files/etc/hickory-dns/forwarder.toml | 123 -- hickory-dns/files/etc/hickory-dns/ipv6_1.zone | 10 - hickory-dns/files/etc/hickory-dns/lan.zone | 11 - .../files/etc/hickory-dns/localhost.zone | 11 - .../files/etc/hotplug.d/iface/99-hickory-dns | 39 - hickory-dns/files/etc/init.d/hickory-dns | 50 - hickory-dns/files/usr/bin/ipv6-neigh-wrapper | 24 - ipv6-neigh/Cargo.lock | 1512 ----------------- ipv6-neigh/Cargo.toml | 29 - ipv6-neigh/Makefile | 70 - ipv6-neigh/src/db.rs | 387 ----- ipv6-neigh/src/filter.rs | 69 - ipv6-neigh/src/iface.rs | 152 -- ipv6-neigh/src/main.rs | 715 -------- ipv6-neigh/src/op.rs | 118 -- ipv6-neigh/src/probe.rs | 142 -- ipv6-neigh/src/reconcile.rs | 275 --- ipv6-neigh/src/types.rs | 59 - .../htdocs/luci-static/material3/mobile.css | 14 +- 26 files changed, 10 insertions(+), 3852 deletions(-) delete mode 100644 hickory-dns/files/etc/hickory-dns/0.0.10.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/0.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/127.0.0.1.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/255.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/3.168.192.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/forwarder.toml delete mode 100644 hickory-dns/files/etc/hickory-dns/ipv6_1.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/lan.zone delete mode 100644 hickory-dns/files/etc/hickory-dns/localhost.zone delete mode 100644 hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns delete mode 100755 hickory-dns/files/etc/init.d/hickory-dns delete mode 100644 hickory-dns/files/usr/bin/ipv6-neigh-wrapper delete mode 100644 ipv6-neigh/Cargo.lock delete mode 100644 ipv6-neigh/Cargo.toml delete mode 100644 ipv6-neigh/Makefile delete mode 100644 ipv6-neigh/src/db.rs delete mode 100644 ipv6-neigh/src/filter.rs delete mode 100644 ipv6-neigh/src/iface.rs delete mode 100644 ipv6-neigh/src/main.rs delete mode 100644 ipv6-neigh/src/op.rs delete mode 100644 ipv6-neigh/src/probe.rs delete mode 100644 ipv6-neigh/src/reconcile.rs delete mode 100644 ipv6-neigh/src/types.rs diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index e74ec542..1043405a 100644 --- a/hickory-dns/Makefile +++ b/hickory-dns/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hickory-dns PKG_VERSION:=0.26.1 -PKG_RELEASE:=29 +PKG_RELEASE:=30 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)? diff --git a/hickory-dns/files/etc/hickory-dns/0.0.10.zone b/hickory-dns/files/etc/hickory-dns/0.0.10.zone deleted file mode 100644 index 9b6b86ee..00000000 --- a/hickory-dns/files/etc/hickory-dns/0.0.10.zone +++ /dev/null @@ -1,8 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 1 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/0.zone b/hickory-dns/files/etc/hickory-dns/0.zone deleted file mode 100644 index 01c795c8..00000000 --- a/hickory-dns/files/etc/hickory-dns/0.zone +++ /dev/null @@ -1,8 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 199609203 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone b/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone deleted file mode 100644 index b6551760..00000000 --- a/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone +++ /dev/null @@ -1,10 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 199609203 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. - -1 PTR localhost. diff --git a/hickory-dns/files/etc/hickory-dns/255.zone b/hickory-dns/files/etc/hickory-dns/255.zone deleted file mode 100644 index 01c795c8..00000000 --- a/hickory-dns/files/etc/hickory-dns/255.zone +++ /dev/null @@ -1,8 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 199609203 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/3.168.192.zone b/hickory-dns/files/etc/hickory-dns/3.168.192.zone deleted file mode 100644 index 9b6b86ee..00000000 --- a/hickory-dns/files/etc/hickory-dns/3.168.192.zone +++ /dev/null @@ -1,8 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 1 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone b/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone deleted file mode 100644 index 9b6b86ee..00000000 --- a/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone +++ /dev/null @@ -1,8 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 1 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/forwarder.toml b/hickory-dns/files/etc/hickory-dns/forwarder.toml deleted file mode 100644 index 37516291..00000000 --- a/hickory-dns/files/etc/hickory-dns/forwarder.toml +++ /dev/null @@ -1,123 +0,0 @@ -listen_addrs_ipv6 = ["::"] -listen_addrs_ipv4 = ["0.0.0.0"] -listen_port = 53 -prometheus_listen_addr = "[::]:9000" -user = "nobody" -group = "nogroup" - -[[zones]] -zone = "localhost" -zone_type = "Primary" -file = "/etc/hickory-dns/localhost.zone" - -[[zones]] -zone = "0.0.127.in-addr.arpa" -zone_type = "Secondary" -file = "/etc/hickory-dns/127.0.0.1.zone" - -[[zones]] -zone = "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" -zone_type = "Primary" -file = "/etc/hickory-dns/ipv6_1.zone" - -[[zones]] -zone = "255.in-addr.arpa" -zone_type = "Primary" -file = "/etc/hickory-dns/255.zone" - -[[zones]] -zone = "0.in-addr.arpa" -zone_type = "Primary" -file = "/etc/hickory-dns/0.zone" - -[[zones]] -zone = "lan" -zone_type = "Primary" -axfr_policy = "AllowAll" - -[zones.stores] -type = "sqlite" -zone_path = "/etc/hickory-dns/lan.zone" -journal_path = ":memory:" -allow_update = true - -[[zones.stores.tsig_keys]] -name = "update-key." -key_file = "/etc/hickory-dns/update.key" -algorithm = "hmac-sha256" -fudge = 300 - -[[zones]] -zone = "d.f.ip6.arpa" -zone_type = "Primary" -axfr_policy = "AllowAll" - -[zones.stores] -type = "sqlite" -zone_path = "/etc/hickory-dns/d.f.ip6.arpa.zone" -journal_path = ":memory:" -allow_update = true - -[[zones.stores.tsig_keys]] -name = "update-key." -key_file = "/etc/hickory-dns/update.key" -algorithm = "hmac-sha256" -fudge = 300 - -[[zones]] -zone = "0.0.10.in-addr.arpa" -zone_type = "Primary" -axfr_policy = "AllowAll" - -[zones.stores] -type = "sqlite" -zone_path = "/etc/hickory-dns/0.0.10.zone" -journal_path = ":memory:" -allow_update = true - -[[zones.stores.tsig_keys]] -name = "update-key." -key_file = "/etc/hickory-dns/update.key" -algorithm = "hmac-sha256" -fudge = 300 - -[[zones]] -zone = "3.168.192.in-addr.arpa" -zone_type = "Primary" -axfr_policy = "AllowAll" - -[zones.stores] -type = "sqlite" -zone_path = "/etc/hickory-dns/3.168.192.zone" -journal_path = ":memory:" -allow_update = true - -[[zones.stores.tsig_keys]] -name = "update-key." -key_file = "/etc/hickory-dns/update.key" -algorithm = "hmac-sha256" -fudge = 300 - -[[zones]] -zone = "." -zone_type = "External" - -[[zones.stores]] -type = "forward" -options = { edns0 = true, ip_strategy = "Ipv6AndIpv4", cache_size = 8192, use_hosts_file = "Always", num_concurrent_reqs = 1, server_ordering_strategy = "RoundRobin" } - -[[zones.stores.name_servers]] -ip = "2400:3200:baba::1" -trust_negative_responses = true -connections = [ - { protocol = { type = "quic", server_name = "dns.alidns.com" } }, - { protocol = { type = "h3", server_name = "dns.alidns.com", path = "/dns-query", disable_grease = false } } -] - -[[zones.stores.name_servers]] -ip = "2400:3200::1" -trust_negative_responses = true -connections = [ - { protocol = { type = "quic", server_name = "dns.alidns.com" } }, - { protocol = { type = "h3", server_name = "dns.alidns.com", path = "/dns-query", disable_grease = false } } -] diff --git a/hickory-dns/files/etc/hickory-dns/ipv6_1.zone b/hickory-dns/files/etc/hickory-dns/ipv6_1.zone deleted file mode 100644 index b6551760..00000000 --- a/hickory-dns/files/etc/hickory-dns/ipv6_1.zone +++ /dev/null @@ -1,10 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 199609203 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. - -1 PTR localhost. diff --git a/hickory-dns/files/etc/hickory-dns/lan.zone b/hickory-dns/files/etc/hickory-dns/lan.zone deleted file mode 100644 index bd1e3828..00000000 --- a/hickory-dns/files/etc/hickory-dns/lan.zone +++ /dev/null @@ -1,11 +0,0 @@ -$TTL 60 -@ IN SOA ns.lan. admin.lan. ( - 1 ; Serial - 3600 ; Refresh - 600 ; Retry - 86400 ; Expire - 60 ) ; Negative caching TTL - - IN NS ns.lan. - -ns IN AAAA ::1 diff --git a/hickory-dns/files/etc/hickory-dns/localhost.zone b/hickory-dns/files/etc/hickory-dns/localhost.zone deleted file mode 100644 index 39966e07..00000000 --- a/hickory-dns/files/etc/hickory-dns/localhost.zone +++ /dev/null @@ -1,11 +0,0 @@ -$TTL 3D -@ IN SOA ns.lan. admin.lan. ( - 199609203 ; Serial - 28800 ; Refresh - 7200 ; Retry - 604800 ; Expire - 86400) ; Minimum TTL - NS ns.lan. - -localhost. A 127.0.0.1 - AAAA ::1 diff --git a/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns b/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns deleted file mode 100644 index 149eeae0..00000000 --- a/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh - -. /lib/functions.sh - -IF_TO_WATCH="wan_6" -SERVICE="hickory-dns" -DELAY=5 -LOCK_DIR="/tmp/hickory-hotplug.lock" -TAG="hickory-hotplug" - -[ "$INTERFACE" = "$IF_TO_WATCH" ] || exit 0 -[ "$ACTION" = "ifup" ] || exit 0 - -/etc/init.d/${SERVICE} enabled || { - logger -t "$TAG" "${SERVICE} not enabled, skipping restart" - exit 0 -} - -mkdir "$LOCK_DIR" 2>/dev/null || { - logger -t "$TAG" "Restart already pending, skipping" - exit 0 -} - -logger -t "$TAG" "${IF_TO_WATCH} ifup - ${SERVICE} restart in ${DELAY}s" - -( - sleep "$DELAY" - - /etc/init.d/${SERVICE} restart >/dev/null 2>&1 - RET=$? - - if [ "$RET" = "0" ]; then - logger -t "$TAG" "${SERVICE} restarted successfully after ${IF_TO_WATCH} ifup" - else - logger -t "$TAG" "${SERVICE} restart failed after ${IF_TO_WATCH} ifup, exit code ${RET}" - fi - - rmdir "$LOCK_DIR" -) & diff --git a/hickory-dns/files/etc/init.d/hickory-dns b/hickory-dns/files/etc/init.d/hickory-dns deleted file mode 100755 index a13bc970..00000000 --- a/hickory-dns/files/etc/init.d/hickory-dns +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh /etc/rc.common - -USE_PROCD=1 -START=18 -STOP=51 - -PROG=/usr/bin/hickory-dns -CONF=/etc/hickory-dns/forwarder.toml -CONF_DIR=/etc/hickory-dns -TSIG_KEY=/etc/hickory-dns/update.key - -RUN_USER=nobody -RUN_GROUP=nogroup - -prepare_config_dir() { - [ -d "$CONF_DIR" ] || mkdir -p "$CONF_DIR" - - chown -R "$RUN_USER:$RUN_GROUP" "$CONF_DIR" 2>/dev/null || true - - find "$CONF_DIR" -type d -exec chmod 755 {} \; 2>/dev/null || true - find "$CONF_DIR" -type f -exec chmod 644 {} \; 2>/dev/null || true -} - -generate_tsig_key() { - if [ ! -f "$TSIG_KEY" ]; then - dd if=/dev/urandom bs=32 count=1 of="$TSIG_KEY" 2>/dev/null - fi - - chown "$RUN_USER:$RUN_GROUP" "$TSIG_KEY" 2>/dev/null || true - chmod 400 "$TSIG_KEY" 2>/dev/null || true -} - -start_service() { - prepare_config_dir - generate_tsig_key - - procd_open_instance hickory-dns - procd_set_param command "$PROG" -c "$CONF" -q - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" - procd_close_instance hickory-dns - - procd_open_instance ipv6-neigh - procd_set_param command /usr/bin/ipv6-neigh-wrapper - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" - procd_close_instance ipv6-neigh -} diff --git a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper b/hickory-dns/files/usr/bin/ipv6-neigh-wrapper deleted file mode 100644 index 66619c08..00000000 --- a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -NEIGH_PROG=/usr/bin/ipv6-neigh -RETRY=30 - -trap 'exit 1' TERM INT - -logger -t ipv6-neigh-wrapper "waiting for DNS port 53" - -i=0 -while [ "$i" -lt "$RETRY" ]; do - if ss -lnt | grep -q ':53'; then - logger -t ipv6-neigh-wrapper "DNS port 53 ready, starting ipv6-neigh" - exec "$NEIGH_PROG" --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router --keepalive-gua \ - --ptr-ipv4-subnet 10.0.0.0/24 \ - --ptr-ipv4-subnet 192.168.3.0/24 \ - --ptr-ula - fi - i=$((i + 1)) - sleep 1 -done - -logger -t ipv6-neigh-wrapper "DNS port 53 not ready after ${RETRY}s, exiting" -exit 1 diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock deleted file mode 100644 index 2fd1f793..00000000 --- a/ipv6-neigh/Cargo.lock +++ /dev/null @@ -1,1512 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "data-encoding" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-proto" -version = "0.27.0-alpha.1" -source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=33cefb6e87b7ef16fb378680b8051c16013ed969#33cefb6e87b7ef16fb378680b8051c16013ed969" -dependencies = [ - "bitflags", - "data-encoding", - "idna", - "ipnet", - "once_cell", - "rand", - "ring", - "rustls-pki-types", - "thiserror 2.0.18", - "time", - "tinyvec", - "tracing", - "url", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "ipv6-neigh" -version = "0.1.0" -dependencies = [ - "clap", - "env_logger", - "futures", - "hickory-proto", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "rtnetlink", - "serde_json", - "socket2", - "tokio", - "ubus", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" - -[[package]] -name = "memchr" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "netlink-packet-core" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" -dependencies = [ - "paste", -] - -[[package]] -name = "netlink-packet-route" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" -dependencies = [ - "bitflags", - "libc", - "log", - "netlink-packet-core", -] - -[[package]] -name = "netlink-proto" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "netlink-sys" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" -dependencies = [ - "bytes", - "futures-util", - "libc", - "log", - "tokio", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rtnetlink" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" -dependencies = [ - "futures-channel", - "futures-util", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "netlink-sys", - "nix", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" -dependencies = [ - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "storage_endian" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "450890d265e3f042570935184b2e429a50b51f487b7d0a443eb6489dd1e0c4d6" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "ubus" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d5669562d66269640d72933c3cc524aaf9a231a3ac50569b9924725ed21a8ed" -dependencies = [ - "serde", - "serde_json", - "storage_endian", - "thiserror 1.0.69", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml deleted file mode 100644 index 9c869d96..00000000 --- a/ipv6-neigh/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "ipv6-neigh" -version = "0.1.0" -edition = "2024" - -[workspace] - -[dependencies] -rtnetlink = { version = "0.21.0" } -netlink-sys = { version = "0.8" } -tokio = { version = "1.52.3", default-features = false, features = ["rt", "macros", "net", "time", "io-util"] } -futures = "0.3.32" -netlink-packet-route = {version = "0.30" } -netlink-packet-core= {version ="0.8"} -clap = { version = "4.6", default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] } -hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "33cefb6e87b7ef16fb378680b8051c16013ed969", features = ["dnssec-ring"] } -ubus = "0.1.7" -serde_json = "1.0.150" -log = "0.4" -env_logger = "0.11" -socket2 = { version = "0.6", features = ["all"] } - -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -strip = true -overflow-checks = false -panic = "abort" diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile deleted file mode 100644 index 002cd5bd..00000000 --- a/ipv6-neigh/Makefile +++ /dev/null @@ -1,70 +0,0 @@ -include $(TOPDIR)/rules.mk - -PKG_NAME:=ipv6-neigh -PKG_VERSION:=0.1.0 -PKG_RELEASE:=19 - -PKG_BUILD_DEPENDS:=rust/host -PKG_BUILD_PARALLEL:=1 - -RUST_PKG_LOCKED:=0 - -include $(INCLUDE_DIR)/package.mk -include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk - -# Derive Rust CPU flags from TARGET_CFLAGS (e.g. -mcpu=cortex-a53+crc+crypto) -_comma:=, -_empty:= -_space:=$(_empty) $(_empty) -_mcpu:=$(patsubst -mcpu=%,%,$(filter -mcpu=%,$(TARGET_CFLAGS))) -_mcpu_parts:=$(subst +,$(_space),$(_mcpu)) -_cpu_base:=$(firstword $(_mcpu_parts)) -_cpu_feats:=$(subst $(_space),$(_comma),$(addprefix +,$(wordlist 2,99,$(_mcpu_parts)))) -PKG_RUST_CPU_FLAGS:=$(if $(_cpu_base),-Ctarget-cpu=$(_cpu_base)$(if $(_cpu_feats), -Ctarget-feature=$(_cpu_feats))) - -define Package/ipv6-neigh - SECTION:=net - CATEGORY:=Network - SUBMENU:=IP Addresses and Names - TITLE:=LAN neighbor DNS updater for hickory-dns - URL:=https://github.com/nicebonn/hickory-dns-openwrt - DEPENDS:=$(RUST_ARCH_DEPENDS) +hickory-dns -endef - -define Package/ipv6-neigh/description - Monitors Linux neighbor table (ARP/NDP) via netlink and dynamically - updates DNS records in a local hickory-dns server using RFC 2136 - DNS UPDATE over TCP. Maps MAC addresses to hostnames via OpenWrt - DHCP leases (ubus). -endef - -define Build/Prepare - mkdir -p $(PKG_BUILD_DIR)/src - $(CP) $(CURDIR)/src/. $(PKG_BUILD_DIR)/src/ - $(CP) $(CURDIR)/Cargo.toml $(PKG_BUILD_DIR)/ - $(CP) $(CURDIR)/Cargo.lock $(PKG_BUILD_DIR)/ -endef - -define Build/Compile - +$(CARGO_PKG_VARS) \ - CARGO_PROFILE_RELEASE_OPT_LEVEL=3 \ - CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS=false \ - CARGO_PROFILE_RELEASE_PANIC=abort \ - RUSTFLAGS="$(CARGO_RUSTFLAGS) $(PKG_RUST_CPU_FLAGS)" \ - cargo build -v --profile $(CARGO_PKG_PROFILE) \ - --manifest-path "$(PKG_BUILD_DIR)/Cargo.toml" \ - --target $(RUSTC_TARGET_ARCH) \ - --no-default-features \ - $(if $(filter --jobserver%,$(PKG_JOBS)),,-j1) \ - $(CARGO_PKG_ARGS) - +$(INSTALL_DIR) $(PKG_INSTALL_DIR)/bin/ - +$(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/$(CARGO_PKG_PROFILE)/ipv6-neigh $(PKG_INSTALL_DIR)/bin/ -endef - -define Package/ipv6-neigh/install - $(INSTALL_DIR) $(1)/usr/bin/ - $(INSTALL_BIN) $(PKG_INSTALL_DIR)/bin/ipv6-neigh $(1)/usr/bin/ipv6-neigh -endef - -$(eval $(call RustBinPackage,ipv6-neigh)) -$(eval $(call BuildPackage,ipv6-neigh)) diff --git a/ipv6-neigh/src/db.rs b/ipv6-neigh/src/db.rs deleted file mode 100644 index 2c806b8b..00000000 --- a/ipv6-neigh/src/db.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::cell::RefCell; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use hickory_proto::op::{Message, Query, ResponseCode, update_message}; -use hickory_proto::rr::{Name, RData, Record, RecordSet, RecordType, TSigner}; -use hickory_proto::rr::rdata::PTR as PtrRData; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::time::timeout; - -use crate::filter::if_ipv6_in_private_subnet; - -const TCP_TIMEOUT: Duration = Duration::from_secs(5); - -fn check_response(response: &Message, allowed: &[ResponseCode]) -> Result<(), Box> { - let code = response.metadata.response_code; - if code == ResponseCode::NoError || allowed.contains(&code) { - Ok(()) - } else { - Err(format!("DNS server returned {code:?}").into()) - } -} - -/// DNS dynamic update client that sends RFC 2136 updates over TCP to the hickory-dns server. -/// TCP connections are reused across calls to reduce syscall overhead. -/// -/// Uses `RefCell` for interior mutability — safe because the entire program runs on a -/// single-threaded tokio runtime (`current_thread`). -pub(crate) struct DnsUpdater { - server_addr: SocketAddr, - zone: Name, - signer: TSigner, - /// (network as u32, prefix_len, reverse zone name) for IPv4 PTR - ipv4_ptr_zones: Vec<(u32, u8, Name)>, - /// Reverse zone for ULA IPv6 (d.f.ip6.arpa) - ula_ptr_zone: Option, - /// Reusable TCP connection (lazy connect + auto-reconnect on failure). - conn: RefCell>, -} - -impl DnsUpdater { - pub fn new(server_addr: SocketAddr, zone: Name, signer: TSigner) -> Self { - Self { - server_addr, - zone, - signer, - ipv4_ptr_zones: vec![], - ula_ptr_zone: None, - conn: RefCell::new(None), - } - } - - /// Configure reverse PTR zones. - /// `ipv4_subnets`: list of (network_addr, prefix_len); /8, /16, /24 boundaries supported. - /// `ula`: if true, enables ULA PTR via `d.f.ip6.arpa`. - pub fn with_ptr_zones(mut self, ipv4_subnets: &[(Ipv4Addr, u8)], ula: bool) -> Self { - self.ipv4_ptr_zones = ipv4_subnets - .iter() - .map(|(net, prefix_len)| (u32::from(*net), *prefix_len, ipv4_zone_name(*net, *prefix_len))) - .collect(); - if ula { - self.ula_ptr_zone = Some(Name::from_ascii("d.f.ip6.arpa.").expect("always valid")); - } - self - } - - // -- generic helpers (eliminates upsert_a/upsert_aaaa and delete_a/delete_aaaa duplication) -- - - /// Create-or-append a record (A or AAAA). Tries CREATE first; on YXRRSet - /// (name already exists) falls back to APPEND so multiple addresses per host work. - async fn upsert_record( - &self, - hostname: &str, - rtype: RecordType, - rdata: RData, - ttl: u32, - ) -> Result<(), Box> { - let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?; - let mut rrset = RecordSet::with_ttl(name.clone(), rtype, ttl); - rrset.add_rdata(rdata.clone()); - let msg = update_message::create(rrset, self.zone.clone(), false); - let response = self.send_tcp(msg).await?; - if response.metadata.response_code == ResponseCode::YXRRSet { - let mut rrset = RecordSet::with_ttl(name, rtype, ttl); - rrset.add_rdata(rdata); - let msg = update_message::append(rrset, self.zone.clone(), false, false); - let response = self.send_tcp(msg).await?; - check_response(&response, &[])?; - } else { - check_response(&response, &[])?; - } - Ok(()) - } - - /// Delete a single record by rdata (A or AAAA). - async fn delete_record( - &self, - hostname: &str, - rtype: RecordType, - rdata: RData, - ) -> Result<(), Box> { - let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?; - let mut rrset = RecordSet::new(name, rtype, 0); - rrset.add_rdata(rdata); - let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false); - let response = self.send_tcp(msg).await?; - check_response(&response, &[])?; - Ok(()) - } - - // -- public A / AAAA wrappers -- - - /// Create or append a AAAA record. - pub async fn upsert_aaaa(&self, hostname: &str, addr: Ipv6Addr, ttl: u32) -> Result<(), Box> { - self.upsert_record(hostname, RecordType::AAAA, RData::AAAA(addr.into()), ttl).await - } - - /// Create or append an A record. - pub async fn upsert_a(&self, hostname: &str, addr: Ipv4Addr, ttl: u32) -> Result<(), Box> { - self.upsert_record(hostname, RecordType::A, RData::A(addr.into()), ttl).await - } - - /// Delete a specific AAAA record. - pub async fn delete_aaaa(&self, hostname: &str, addr: Ipv6Addr) -> Result<(), Box> { - self.delete_record(hostname, RecordType::AAAA, RData::AAAA(addr.into())).await - } - - /// Delete a specific A record. - pub async fn delete_a(&self, hostname: &str, addr: Ipv4Addr) -> Result<(), Box> { - self.delete_record(hostname, RecordType::A, RData::A(addr.into())).await - } - - // -- PTR -- - - /// Upsert a PTR record. Silently skips IPs with no configured reverse zone. - pub async fn upsert_ptr( - &self, - addr: IpAddr, - hostname: &str, - ttl: u32, - ) -> Result<(), Box> { - let (ptr_name, rev_zone) = match addr { - IpAddr::V4(ip) => { - let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) }; - (ipv4_ptr_name(ip), zone) - } - IpAddr::V6(ip) if if_ipv6_in_private_subnet(&ip) => { - let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) }; - (ipv6_ptr_name(ip), zone) - } - _ => return Ok(()), - }; - let target = Name::from_ascii(hostname)?.append_domain(&self.zone)?; - // Delete any existing PTR rrset first (replace semantics: one PTR per IP). - let del_record = Record::update0(ptr_name.clone(), 0, RecordType::PTR); - let del_msg = update_message::delete_rrset(del_record, rev_zone.clone(), false); - let del_resp = self.send_tcp(del_msg).await?; - check_response(&del_resp, &[])?; - // Add the new PTR. - let mut rrset = RecordSet::with_ttl(ptr_name, RecordType::PTR, ttl); - rrset.add_rdata(RData::PTR(PtrRData(target))); - let msg = update_message::append(rrset, rev_zone, false, false); - let response = self.send_tcp(msg).await?; - check_response(&response, &[])?; - Ok(()) - } - - /// Delete a PTR record. Silently skips IPs with no configured reverse zone. - pub async fn delete_ptr( - &self, - addr: IpAddr, - ) -> Result<(), Box> { - let (ptr_name, rev_zone) = match addr { - IpAddr::V4(ip) => { - let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) }; - (ipv4_ptr_name(ip), zone) - } - IpAddr::V6(ip) if if_ipv6_in_private_subnet(&ip) => { - let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) }; - (ipv6_ptr_name(ip), zone) - } - _ => return Ok(()), - }; - let record = Record::update0(ptr_name, 0, RecordType::PTR); - let msg = update_message::delete_rrset(record, rev_zone, false); - let response = self.send_tcp(msg).await?; - check_response(&response, &[])?; - Ok(()) - } - - fn find_ipv4_ptr_zone(&self, addr: Ipv4Addr) -> Option<&Name> { - let addr_u32 = u32::from(addr); - self.ipv4_ptr_zones - .iter() - .filter(|(net, prefix_len, _)| { - if *prefix_len == 0 { return true; } - let shift = 32u8.saturating_sub(*prefix_len); - (addr_u32 >> shift) == (net >> shift) - }) - .max_by_key(|(_, prefix_len, _)| *prefix_len) - .map(|(_, _, name)| name) - } - - // -- TCP transport (connection-reusing) -- - - /// Send a DNS message over TCP and read the response. - /// Reuses an existing connection when possible; reconnects automatically on failure. - async fn send_tcp(&self, mut msg: Message) -> Result> { - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - msg.finalize(&self.signer, now)?; - let bytes = msg.to_vec()?; - let len_bytes = u16::try_from(bytes.len())?.to_be_bytes(); - - // Try the existing connection first. - // Take it out of the RefCell so we don't hold the borrow across await. - let existing = self.conn.borrow_mut().take(); - if let Some(mut stream) = existing { - match Self::do_send_recv(&mut stream, &len_bytes, &bytes, self.server_addr).await { - Ok(resp) => { - // Put the connection back for reuse. - *self.conn.borrow_mut() = Some(stream); - return Ok(resp); - } - Err(_) => { - // Connection broken — will reconnect below. - } - } - } - - // Establish a new connection. - let mut stream = timeout(TCP_TIMEOUT, TcpStream::connect(self.server_addr)) - .await - .map_err(|_| -> Box { - format!("DNS TCP connect timeout after {}s to {}", TCP_TIMEOUT.as_secs(), self.server_addr).into() - })??; - - let resp = Self::do_send_recv(&mut stream, &len_bytes, &bytes, self.server_addr).await?; - *self.conn.borrow_mut() = Some(stream); - Ok(resp) - } - - /// Write a length-prefixed DNS message and read the response on an existing stream. - async fn do_send_recv( - stream: &mut TcpStream, - len_bytes: &[u8], - msg_bytes: &[u8], - server_addr: SocketAddr, - ) -> Result> { - timeout(TCP_TIMEOUT, async { - stream.write_all(len_bytes).await?; - stream.write_all(msg_bytes).await?; - stream.flush().await?; - let resp_len = stream.read_u16().await? as usize; - let mut resp_buf = vec![0u8; resp_len]; - stream.read_exact(&mut resp_buf).await?; - Ok::<_, Box>(Message::from_vec(&resp_buf)?) - }) - .await - .map_err(|_| -> Box { - format!("DNS TCP timeout after {}s to {}", TCP_TIMEOUT.as_secs(), server_addr).into() - })? - } - - // -- AXFR (uses its own connection -- streaming protocol) -- - - /// Fetch all A and AAAA records in the zone via AXFR (RFC 5936). - /// - /// Returns a list of `(hostname_label, IpAddr)` pairs -- only records whose owner - /// name is directly under the zone apex (e.g. `foo.lan.` -> `"foo"`). - /// SOA, NS, and apex records are excluded. - /// - /// Requires `axfr_policy = "AllowAll"` (or `"AllowSigned"`) in the server config. - pub async fn axfr_records(&self) -> Result, Box> { - const AXFR_TIMEOUT: Duration = Duration::from_secs(10); - - let query = Query::new(self.zone.clone(), RecordType::AXFR); - let mut msg = Message::query(); - msg.add_query(query); - let bytes = msg.to_vec()?; - let len = u16::try_from(bytes.len())?; - - let zone = self.zone.clone(); - let addr = self.server_addr; - - let records = timeout(AXFR_TIMEOUT, async move { - let mut stream = TcpStream::connect(addr).await?; - stream.write_all(&len.to_be_bytes()).await?; - stream.write_all(&bytes).await?; - stream.flush().await?; - - let mut records: Vec<(String, IpAddr)> = Vec::new(); - let mut soa_count = 0u32; - - loop { - let resp_len = stream.read_u16().await? as usize; - if resp_len == 0 { - break; - } - let mut buf = vec![0u8; resp_len]; - stream.read_exact(&mut buf).await?; - let response = Message::from_vec(&buf)?; - if response.metadata.response_code != ResponseCode::NoError { - return Err(format!("AXFR error: {:?}", response.metadata.response_code).into()); - } - for record in &response.answers { - match &record.data { - RData::SOA(_) => { - soa_count += 1; - } - RData::A(a) => { - if let Some(host) = extract_hostname(&record.name, &zone) { - records.push((host, IpAddr::V4(a.0))); - } - } - RData::AAAA(aaaa) => { - if let Some(host) = extract_hostname(&record.name, &zone) { - records.push((host, IpAddr::V6(aaaa.0))); - } - } - _ => {} - } - } - if soa_count >= 2 { - break; - } - } - Ok::<_, Box>(records) - }) - .await - .map_err(|_| -> Box { - format!("AXFR timeout after {}s connecting to {}", AXFR_TIMEOUT.as_secs(), self.server_addr).into() - })??; - Ok(records) - } -} - -/// Build the PTR owner name for an IPv4 address. -/// e.g. 192.168.3.5 -> `5.3.168.192.in-addr.arpa.` -fn ipv4_ptr_name(addr: Ipv4Addr) -> Name { - let o = addr.octets(); - Name::from_ascii(&format!("{}.{}.{}.{}.in-addr.arpa.", o[3], o[2], o[1], o[0])) - .expect("always valid") -} - -/// Build the PTR owner name for an IPv6 address (nibble-reversed). -fn ipv6_ptr_name(addr: Ipv6Addr) -> Name { - let nibbles: String = addr - .octets() - .iter() - .rev() - .flat_map(|b| { - let lo = char::from_digit((b & 0x0f) as u32, 16).unwrap(); - let hi = char::from_digit((b >> 4) as u32, 16).unwrap(); - [lo, '.', hi, '.'] - }) - .collect(); - Name::from_ascii(&format!("{}ip6.arpa.", nibbles)).expect("always valid") -} - -/// Derive the reverse zone name for an IPv4 subnet (only /8, /16, /24 boundaries). -fn ipv4_zone_name(net: Ipv4Addr, prefix_len: u8) -> Name { - let o = net.octets(); - let s = match prefix_len { - 24..=32 => format!("{}.{}.{}.in-addr.arpa.", o[2], o[1], o[0]), - 16..=23 => format!("{}.{}.in-addr.arpa.", o[1], o[0]), - 8..=15 => format!("{}.in-addr.arpa.", o[0]), - _ => "in-addr.arpa.".to_string(), - }; - Name::from_ascii(&s).expect("always valid") -} - -/// Extract the single label that precedes the zone apex from a fully-qualified record name. -/// -/// e.g. `"foo.lan."` with zone `"lan."` -> `Some("foo")`. -/// Returns `None` for the apex itself or for names not directly under the zone. -fn extract_hostname(name: &Name, zone: &Name) -> Option { - let n = name.to_ascii().to_lowercase(); - let z = zone.to_ascii().to_lowercase(); - let n = n.trim_end_matches('.'); - let z = z.trim_end_matches('.'); - if n == z { - return None; // apex record - } - let suffix = format!(".{z}"); - n.strip_suffix(&suffix).map(|s| s.to_string()) -} diff --git a/ipv6-neigh/src/filter.rs b/ipv6-neigh/src/filter.rs deleted file mode 100644 index 5349a153..00000000 --- a/ipv6-neigh/src/filter.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::net::{Ipv4Addr, Ipv6Addr}; -use crate::types::LanPrefix; - -/// Returns true if `addr` is link-local (fe80::/10). -/// Centralised here so every module uses the same check. -pub(crate) fn is_link_local_ipv6(addr: &Ipv6Addr) -> bool { - (addr.segments()[0] & 0xffc0) == 0xfe80 -} - -/// Returns true if `addr` is ULA (fc00::/7). -pub(crate) fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool { - (ip.segments()[0] & 0xfe00) == 0xfc00 -} - -/// Returns true if `addr` is a Global Unicast Address (2000::/3). -pub(crate) fn is_gua_ipv6(addr: &Ipv6Addr) -> bool { - (addr.segments()[0] & 0xe000) == 0x2000 -} - -/// Returns true if this IPv6 address should be skipped for LAN purposes. -/// Skips link-local always; skips non-ULA when `private_subnet_v6` is set. -pub(crate) fn should_skip_v6(addr: &Ipv6Addr, private_subnet_v6: bool) -> bool { - if is_link_local_ipv6(addr) { - return true; - } - if private_subnet_v6 && !if_ipv6_in_private_subnet(addr) { - return true; - } - false -} - -pub(crate) fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool { - let octets = ip.octets(); - // 10.0.0.0/8 - if octets[0] == 10 { - return true; - } - // 172.16.0.0/12 - if octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31) { - return true; - } - // 192.168.0.0/16 - if octets[0] == 192 && octets[1] == 168 { - return true; - } - // 127.0.0.0/8 (loopback) - if octets[0] == 127 { - return true; - } - false -} - -/// Returns true if `addr` falls within the given IPv6 prefix. -pub(crate) fn ipv6_in_prefix(addr: Ipv6Addr, prefix: &LanPrefix) -> bool { - let mask: u128 = if prefix.prefix_len == 0 { - 0 - } else if prefix.prefix_len >= 128 { - u128::MAX - } else { - !0u128 << (128 - prefix.prefix_len) - }; - (u128::from(addr) & mask) == (u128::from(prefix.addr) & mask) -} - -/// Returns true if `addr` is within any active LAN prefix, or if the prefix list is empty -/// (in which case prefix-based filtering is disabled). -pub(crate) fn ipv6_passes_active_prefix(addr: Ipv6Addr, active_prefixes: &[LanPrefix]) -> bool { - active_prefixes.is_empty() || active_prefixes.iter().any(|p| ipv6_in_prefix(addr, p)) -} diff --git a/ipv6-neigh/src/iface.rs b/ipv6-neigh/src/iface.rs deleted file mode 100644 index 88e34220..00000000 --- a/ipv6-neigh/src/iface.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; -use futures::stream::TryStreamExt; -use log::{info, warn}; -use netlink_packet_route::address::{AddressAttribute, AddressFlags, AddressHeaderFlags, AddressMessage}; -use rtnetlink::Handle; -use tokio::time::{self, Duration}; - -use crate::db::DnsUpdater; -use crate::filter::should_skip_v6; -use crate::types::{DEFAULT_TTL, LanPrefix}; - -/// Check whether an address message is deprecated (preferred lifetime expired). -pub(crate) fn is_addr_deprecated(msg: &AddressMessage) -> bool { - if msg.header.flags.contains(AddressHeaderFlags::Deprecated) { - return true; - } - for attr in &msg.attributes { - match attr { - AddressAttribute::Flags(f) if f.contains(AddressFlags::Deprecated) => return true, - AddressAttribute::CacheInfo(ci) if ci.ifa_preferred == 0 => return true, - _ => {} - } - } - false -} - -/// Look up the interface index and collect all address messages for `iface`. -/// Shared between `get_active_lan_prefixes` and `register_router_addresses` -/// to avoid duplicating the link-lookup + address-enumeration boilerplate. -async fn get_iface_addresses( - handle: Handle, - iface: &str, -) -> Result<(u32, Vec), String> { - let mut links = handle.link().get().match_name(iface.to_owned()).execute(); - let link = match links.try_next().await { - Ok(Some(l)) => l, - Ok(None) => return Err(format!("interface {} not found", iface)), - Err(e) => return Err(format!("failed to find interface {}: {}", iface, e)), - }; - let ifindex = link.header.index; - let mut addr_stream = handle.address().get().set_link_index_filter(ifindex).execute(); - let mut msgs = Vec::new(); - while let Ok(Some(msg)) = addr_stream.try_next().await { - msgs.push(msg); - } - Ok((ifindex, msgs)) -} - -/// Enumerate non-link-local IPv6 prefixes currently assigned to `iface`. -/// Used during startup to filter out neighbour entries from expired/old prefixes. -pub(crate) async fn get_active_lan_prefixes( - handle: Handle, - iface: &str, - private_subnet_v6: bool, -) -> Vec { - let (_ifindex, msgs) = match get_iface_addresses(handle, iface).await { - Ok(v) => v, - Err(_) => return vec![], - }; - - let mut prefixes = Vec::new(); - for msg in &msgs { - let prefix_len = msg.header.prefix_len; - if is_addr_deprecated(msg) { - continue; - } - for attr in &msg.attributes { - if let AddressAttribute::Address(IpAddr::V6(addr)) = attr { - if should_skip_v6(addr, private_subnet_v6) { - continue; - } - prefixes.push(LanPrefix { addr: *addr, prefix_len }); - } - } - } - prefixes -} - -/// Enumerate addresses on `iface`, register A/AAAA records for the router itself. -pub(crate) async fn register_router_addresses( - handle: Handle, - iface: &str, - hostname: &str, - extra_alias: Option<&str>, - updater: &DnsUpdater, - private_subnet_v6: bool, -) { - let (_ifindex, msgs) = match get_iface_addresses(handle, iface).await { - Ok(v) => v, - Err(e) => { - warn!("{}, skipping router address registration", e); - return; - } - }; - - for msg in &msgs { - if is_addr_deprecated(msg) { - continue; - } - for attr in &msg.attributes { - let ip = match attr { - AddressAttribute::Address(ip) => ip, - _ => continue, - }; - match ip { - IpAddr::V6(addr) => { - if should_skip_v6(addr, private_subnet_v6) { - continue; - } - for name in std::iter::once(hostname).chain(extra_alias) { - match updater.upsert_aaaa(name, *addr, DEFAULT_TTL).await { - Ok(()) => info!("registered router {} AAAA {}", name, addr), - Err(e) => warn!("failed to register router AAAA {} for {}: {}", addr, name, e), - } - } - if let Err(e) = updater.upsert_ptr(IpAddr::V6(*addr), hostname, DEFAULT_TTL).await { - warn!("failed to register router PTR for {}: {}", addr, e); - } - } - IpAddr::V4(addr) => { - for name in std::iter::once(hostname).chain(extra_alias) { - match updater.upsert_a(name, *addr, DEFAULT_TTL).await { - Ok(()) => info!("registered router {} A {}", name, addr), - Err(e) => warn!("failed to register router A {} for {}: {}", addr, name, e), - } - } - if let Err(e) = updater.upsert_ptr(IpAddr::V4(*addr), hostname, DEFAULT_TTL).await { - warn!("failed to register router PTR for {}: {}", addr, e); - } - } - } - } - } -} - -/// Block until a TCP connection to `addr` succeeds, retrying every 2 seconds. -/// This ensures the DNS server is ready before we attempt any dynamic updates. -pub(crate) async fn wait_for_dns_server(addr: SocketAddr) { - use tokio::net::TcpStream; - loop { - match TcpStream::connect(addr).await { - Ok(_) => { - info!("DNS server at {} is reachable, proceeding", addr); - return; - } - Err(e) => { - warn!("DNS server at {} not ready ({}), retrying in 2s...", addr, e); - time::sleep(Duration::from_secs(2)).await; - } - } - } -} diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs deleted file mode 100644 index a904f047..00000000 --- a/ipv6-neigh/src/main.rs +++ /dev/null @@ -1,715 +0,0 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr as StdSocketAddr}; -use std::time::Instant; - -use futures::stream::StreamExt; -use futures::stream::TryStreamExt; -use log::{debug, info, warn}; -use netlink_packet_core::NetlinkPayload; -use netlink_packet_route::RouteNetlinkMessage; -use netlink_packet_route::neighbour::NeighbourMessage; -use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState}; -use netlink_packet_route::route::RouteType; -use rtnetlink::{Error, Handle, new_connection}; -use tokio::time::{self, Duration}; - -use clap::Parser; -use hickory_proto::rr::Name; -use hickory_proto::rr::TSigner; -use hickory_proto::rr::rdata::tsig::TsigAlgorithm; -use netlink_sys::{AsyncSocket, SocketAddr}; - -mod db; -mod filter; -mod iface; -mod op; -mod probe; -mod reconcile; -mod types; - -use filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, ipv6_passes_active_prefix, is_gua_ipv6}; -use iface::{get_active_lan_prefixes, register_router_addresses, wait_for_dns_server}; -use probe::{Prober, probe_gua_keepalive, probe_registered_neighbours, prune_gua_keepalive}; -use reconcile::{do_delete_dns, process_new_neigh, prune_ula_for_host, reconcile_dns}; -use types::{ - RTNLGRP_NEIGH, GuaKeepaliveEntry, Neigh, RegisteredEntry, format_mac, nl_mgrp, -}; - -#[derive(Debug, Parser)] -#[clap()] -struct Cli { - /// Restrict IPv4 neighbours to private subnets only (10/8, 172.16/12, 192.168/16, 127/8) - #[clap(long)] - private_subnet_v4: bool, - - /// Also publish GUA (Global Unicast Address) AAAA records; by default only ULA (fc00::/7) is published - #[clap(long)] - publish_gua: bool, - - /// hickory-dns server address for DNS updates (e.g. "[::1]:5335") - #[clap(short, long, default_value = "[::1]:5335")] - dns_server: StdSocketAddr, - - /// DNS zone to update (e.g. "lan") - #[clap(short, long, default_value = "lan")] - zone: String, - - /// Path to TSIG key file (raw HMAC secret) - #[clap(short, long, default_value = "/etc/hickory-dns/update.key")] - key_file: std::path::PathBuf, - - /// TSIG key name - #[clap(short = 'n', long, default_value = "update-key.")] - key_name: String, - - /// Log level (error, warn, info, debug, trace) - #[clap(short = 'l', long, default_value = "info")] - log_level: String, - - /// Probe interval in seconds for active reachability checks (0 to disable) - #[clap(long, default_value = "75")] - probe_interval: u64, - - /// Network interface to read the router's own addresses from - #[clap(long, default_value = "br-lan")] - router_iface: String, - - /// Additional DNS name to register router addresses under (e.g. "router" -> "router.lan") - #[clap(long)] - router_alias: Option, - - /// Periodically probe the newest GUA per host to maintain NUD/Wi-Fi reachability (without publishing to DNS) - #[clap(long)] - keepalive_gua: bool, - - /// Probe interval in seconds for GUA keepalive (only used with --keepalive-gua) - #[clap(long, default_value = "120")] - keepalive_gua_interval: u64, - - /// Maximum number of GUA addresses to keepalive-probe per host - #[clap(long, default_value = "2")] - keepalive_gua_per_host: usize, - - /// Maximum number of ULA AAAA records to publish per host (oldest pruned first) - #[clap(long, default_value = "2")] - max_ula_per_host: usize, - - /// IPv4 subnets for PTR reverse DNS (CIDR notation, e.g. "192.168.3.0/24", repeatable) - #[clap(long, value_name = "SUBNET")] - ptr_ipv4_subnet: Vec, - - /// Enable PTR reverse DNS for ULA IPv6 addresses (fc00::/7) via d.f.ip6.arpa - #[clap(long)] - ptr_ula: bool, -} - -fn should_skip_route_type(route_type: RouteType) -> bool { - matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local) -} - -fn should_skip_neigh(neigh: &Neigh) -> bool { - should_skip_route_type(neigh.kind) -} - -/// Whether this NUD state indicates the neighbour is definitely gone (remove from DNS). -fn is_failed_state(state: NeighbourState) -> bool { - matches!(state, NeighbourState::Failed) -} - -/// Parse an IPv4 CIDR string like "192.168.3.0/24" into (network_addr, prefix_len). -fn parse_ipv4_cidr(s: &str) -> (Ipv4Addr, u8) { - let (addr_str, prefix_str) = s.split_once('/').unwrap_or_else(|| { - panic!("invalid --ptr-ipv4-subnet \"{s}\": expected CIDR notation like \"192.168.3.0/24\"") - }); - let addr: Ipv4Addr = addr_str.parse().unwrap_or_else(|_| { - panic!("invalid --ptr-ipv4-subnet \"{s}\": \"{addr_str}\" is not a valid IPv4 address") - }); - let prefix_len: u8 = prefix_str.parse().unwrap_or_else(|_| { - panic!("invalid --ptr-ipv4-subnet \"{s}\": prefix length must be 0-32") - }); - assert!(prefix_len <= 32, "invalid --ptr-ipv4-subnet \"{s}\": prefix length {prefix_len} > 32"); - (addr, prefix_len) -} - -/// Register a new REACHABLE neighbour in DNS and add to the registered map. -/// Handles ULA pruning and delegates to `process_new_neigh` for the actual DNS update. -/// Caller must ensure the (hostname, ip) key is not already in `registered`. -async fn try_register_neigh( - neigh: &Neigh, - hostname: &str, - ip_str: &str, - registered: &mut HashMap<(String, String), RegisteredEntry>, - leases: &HashMap, - updater: &db::DnsUpdater, - max_ula_per_host: usize, - private_subnet_v6: bool, -) { - // Enforce per-host ULA limit before adding. - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if if_ipv6_in_private_subnet(addr) { - prune_ula_for_host(hostname, max_ula_per_host, registered, updater).await; - } - } - if process_new_neigh(neigh, updater, leases, private_subnet_v6).await { - registered.insert( - (hostname.to_owned(), ip_str.to_owned()), - RegisteredEntry { - hostname: hostname.to_owned(), - last_confirmed: Instant::now(), - ifindex: neigh.ifindex, - }, - ); - } -} - -fn inet_to_string(addr: &NeighbourAddress) -> String { - match addr { - NeighbourAddress::Inet(ip) => ip.to_string(), - NeighbourAddress::Inet6(ip) => ip.to_string(), - other => format!("{other:?}"), - } -} - -fn is_link_local_neigh_addr(addr: &NeighbourAddress) -> bool { - matches!(addr, NeighbourAddress::Inet6(ip) if filter::is_link_local_ipv6(ip)) -} - -fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool) -> Option { - let state = neigh.header.state; - - // Filter out static and incomplete entries - if matches!(state, NeighbourState::Permanent | NeighbourState::Noarp) { - return None; - } - - let addr: NeighbourAddress = neigh.attributes.iter().find_map(|attr| match attr { - NeighbourAttribute::Destination(inet) => Some(inet.to_owned()), - _ => None, - })?; - - // Link-local IPv6 addresses are interface-scoped and useless in DNS - if is_link_local_neigh_addr(&addr) { - return None; - } - - // IPv4 private-subnet filter stays here (no keepalive concept for IPv4 GUA) - if let NeighbourAddress::Inet(ipv4) = &addr { - if private_subnet_v4 && !if_ipv4_in_private_subnet(ipv4) { - return None; - } - } - - let kind = neigh.header.kind; - let ifindex = neigh.header.ifindex; - - let mac_bytes = neigh.attributes.iter().find_map(|attr| match attr { - NeighbourAttribute::LinkLayerAddress(mac) => Some(mac.to_owned()), - _ => None, - }); - - // FAILED events may arrive without a hardware address (e.g. when ARP/NDP never - // succeeded, or the kernel cleared the lladdr on failure). Allow them through - // with an empty MAC so the FAILED handler can fall back to IP-based lookup. - let mac_str = match mac_bytes { - Some(m) => format_mac(&m), - None if matches!(state, NeighbourState::Failed) => String::new(), - None => return None, - }; - - // Filter out empty or all-zero MACs for non-FAILED states - // (router own addresses, incomplete entries). - if !matches!(state, NeighbourState::Failed) && (mac_str.is_empty() || mac_str == "00:00:00:00:00:00") { - return None; - } - - Some(Neigh { - ifindex, - state, - kind, - inet: addr, - mac: mac_str, - }) -} - -async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result, Error> { - let mut neighbours = handle.neighbours().get().execute(); - let mut vec: Vec = Vec::new(); - while let Some(route) = neighbours.try_next().await? { - if let Some(neigh) = parse_neighbour_message(route, private_subnet_v4) { - if !should_skip_neigh(&neigh) { - vec.push(neigh); - } - } - } - Ok(vec) -} - -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), ()> { - let args = Cli::parse(); - - env_logger::Builder::new() - .filter_level( - args.log_level - .parse() - .expect("invalid log level (use: error, warn, info, debug, trace)"), - ) - .filter_module("netlink_packet_route", log::LevelFilter::Error) - .format_timestamp_secs() - .init(); - - let private_subnet_v4 = args.private_subnet_v4; - let private_subnet_v6 = !args.publish_gua; - let keepalive_gua = args.keepalive_gua; - let keepalive_gua_interval = args.keepalive_gua_interval; - let keepalive_gua_per_host = args.keepalive_gua_per_host; - let max_ula_per_host = args.max_ula_per_host; - - let zone = Name::from_ascii(&args.zone).expect("invalid zone name"); - let key_data = std::fs::read(&args.key_file) - .unwrap_or_else(|e| panic!("failed to read TSIG key file {:?}: {e}", args.key_file)); - let key_name = Name::from_ascii(&args.key_name).expect("invalid TSIG key name"); - let signer = TSigner::new(key_data, TsigAlgorithm::HmacSha256, key_name, 300) - .expect("invalid TSIG key"); - - let ipv4_ptr_subnets: Vec<(Ipv4Addr, u8)> = args.ptr_ipv4_subnet - .iter() - .map(|s| parse_ipv4_cidr(s)) - .collect(); - - let updater = db::DnsUpdater::new(args.dns_server, zone, signer) - .with_ptr_zones(&ipv4_ptr_subnets, args.ptr_ula); - - // Create reusable ICMP probe sockets once at startup. - let prober = Prober::new().expect("failed to create ICMP probe sockets"); - - // Wait for the DNS server to become available before sending any updates. - wait_for_dns_server(args.dns_server).await; - - let (mut connection, handle, mut messages) = new_connection().unwrap(); - - connection - .socket_mut() - .socket_mut() - .bind(&SocketAddr::new(0, nl_mgrp(RTNLGRP_NEIGH))) - .expect("Failed to bind netlink multicast group"); - - tokio::spawn(connection); - - // Register router's own addresses in DNS - let router_hostname = std::fs::read_to_string("/proc/sys/kernel/hostname") - .unwrap_or_default() - .trim() - .to_owned(); - if router_hostname.is_empty() { - warn!("could not read router hostname from /proc/sys/kernel/hostname"); - } else { - let router_alias = args.router_alias.as_deref().filter(|a| *a != router_hostname); - register_router_addresses( - handle.clone(), - &args.router_iface, - &router_hostname, - router_alias, - &updater, - private_subnet_v6, - ) - .await; - } - - // Load DHCP leases (mac -> hostname) from ubus. - let mut leases: HashMap = tokio::task::spawn_blocking(|| { - op::get_lease().unwrap_or_default() - }) - .await - .unwrap_or_default(); - info!("loaded {} DHCP leases", leases.len()); - - let mut registered: HashMap<(String, String), RegisteredEntry> = HashMap::new(); - let mut gua_keepalive: HashMap> = HashMap::new(); - - let prefix_filter_v6 = if keepalive_gua { false } else { private_subnet_v6 }; - let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, prefix_filter_v6).await; - if active_prefixes.is_empty() { - warn!("no active IPv6 prefixes found on {}, neighbour prefix-filtering disabled", args.router_iface); - } else { - info!( - "active LAN IPv6 prefixes: {}", - active_prefixes - .iter() - .map(|p| format!("{}/{}", p.addr, p.prefix_len)) - .collect::>() - .join(", ") - ); - } - - // Dump existing neighbours - debug!("dumping neighbours"); - if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await { - for neigh in &neighbours { - debug!("{:?}", neigh); - if should_skip_neigh(neigh) { - continue; - } - - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if !active_prefixes.is_empty() && !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)) { - debug!("init dump: skipping {} -- not in any active LAN prefix", addr); - continue; - } - } - - // Route GUA addresses to keepalive map instead of DNS. - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if is_gua_ipv6(addr) { - if keepalive_gua { - if neigh.state == NeighbourState::Reachable { - if let Some(hostname) = leases.get(&neigh.mac) { - let entries = gua_keepalive.entry(neigh.mac.clone()).or_default(); - if !entries.iter().any(|e| e.addr == *addr) { - let now = Instant::now(); - entries.push(GuaKeepaliveEntry { - hostname: hostname.clone(), - addr: *addr, - ifindex: neigh.ifindex, - first_seen: now, - last_confirmed: now, - }); - debug!("init dump: GUA keepalive tracked {} -> {}", hostname, addr); - } - } - } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { - match prober.send_icmpv6_echo(*addr, neigh.ifindex) { - Ok(()) => debug!("init dump: probing stale GUA {}", addr), - Err(e) => debug!("init GUA probe failed for {}: {}", addr, e), - } - } - } - if private_subnet_v6 { - continue; - } - } - } - - let ip_str = inet_to_string(&neigh.inet); - match neigh.state { - NeighbourState::Reachable => { - if let Some(hostname) = leases.get(&neigh.mac) { - let key = (hostname.clone(), ip_str.clone()); - if !registered.contains_key(&key) { - try_register_neigh(neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await; - } - } else { - debug!("no lease for mac {}, skipping DNS update", neigh.mac); - } - } - NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe => { - match &neigh.inet { - NeighbourAddress::Inet6(addr) => { - match prober.send_icmpv6_echo(*addr, neigh.ifindex) { - Ok(()) => debug!("init dump: probing stale neighbour {}", addr), - Err(e) => debug!("init probe failed for {}: {}", addr, e), - } - } - NeighbourAddress::Inet(addr) => { - if let Err(e) = prober.send_icmpv4_echo(*addr, neigh.ifindex) { - debug!("init probe failed for {}: {}", addr, e); - } - } - _ => {} - } - } - _ => {} - } - } - } - - let probe_interval = args.probe_interval; - let mut probe_timer = if probe_interval > 0 { - time::interval(Duration::from_secs(probe_interval)) - } else { - time::interval(Duration::from_secs(u64::MAX / 2)) - }; - probe_timer.tick().await; - - let mut gua_keepalive_timer = if keepalive_gua && keepalive_gua_interval > 0 { - time::interval(Duration::from_secs(keepalive_gua_interval)) - } else { - time::interval(Duration::from_secs(u64::MAX / 2)) - }; - gua_keepalive_timer.tick().await; - - let mut lease_refresh_timer = time::interval(Duration::from_secs(60)); - lease_refresh_timer.tick().await; - - loop { - tokio::select! { - msg_opt = messages.next() => { - let Some((message, _)) = msg_opt else { - break; - }; - let payload = message.payload; - if let NetlinkPayload::InnerMessage(msg) = payload { - match msg { - RouteNetlinkMessage::NewNeighbour(new_neigh) => { - let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet_v4) else { - continue; - }; - if should_skip_neigh(&neigh) { - continue; - } - - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if !ipv6_passes_active_prefix(*addr, &active_prefixes) { - debug!("event: skipping {} -- not in any active LAN prefix", addr); - let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if let Some(entry) = registered.remove(&key) { - do_delete_dns(&entry.hostname, &neigh.inet, &updater).await; - } - if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { - entries.retain(|e| e.addr != *addr); - } - continue; - } - } - - // Route GUA addresses to keepalive map instead of DNS. - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if is_gua_ipv6(addr) && private_subnet_v6 { - if keepalive_gua { - if is_failed_state(neigh.state) { - if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { - entries.retain(|e| e.addr != *addr); - debug!("GUA keepalive: removed failed {} for mac {}", addr, neigh.mac); - } - } else if neigh.state == NeighbourState::Reachable { - if let Some(hostname) = leases.get(&neigh.mac) { - let entries = gua_keepalive.entry(neigh.mac.clone()).or_default(); - if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) { - e.last_confirmed = Instant::now(); - e.ifindex = neigh.ifindex; - } else { - let now = Instant::now(); - entries.push(GuaKeepaliveEntry { - hostname: hostname.clone(), - addr: *addr, - ifindex: neigh.ifindex, - first_seen: now, - last_confirmed: now, - }); - debug!("GUA keepalive: tracking {} -> {}", hostname, addr); - } - } - } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { - if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { - if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) { - e.ifindex = neigh.ifindex; - } - } - } - } - continue; - } - } - - let ip_str = inet_to_string(&neigh.inet); - - if is_failed_state(neigh.state) { - debug!("Neighbour failed: {:?}", neigh); - let key_opt: Option<(String, String)> = leases - .get(&neigh.mac) - .map(|h| (h.clone(), ip_str.clone())) - .or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned()); - - if let Some(key) = key_opt { - if let Some(entry) = registered.remove(&key) { - if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await { - registered.insert(key, entry); - } - } else { - debug!("Neighbour failed: not in registered map, key={:?}", key); - } - } else { - debug!("Neighbour failed: no registered key for ip={}", ip_str); - } - } else if neigh.state == NeighbourState::Reachable { - if let Some(hostname) = leases.get(&neigh.mac) { - let key = (hostname.clone(), ip_str.clone()); - if let Some(entry) = registered.get_mut(&key) { - entry.last_confirmed = Instant::now(); - entry.ifindex = neigh.ifindex; - } else { - debug!("New neighbour: {:?}", neigh); - try_register_neigh(&neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await; - } - } else { - debug!("no lease for mac {}, skipping DNS update", neigh.mac); - } - } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { - if let Some(hostname) = leases.get(&neigh.mac) { - let key = (hostname.clone(), ip_str.clone()); - if let Some(entry) = registered.get_mut(&key) { - entry.ifindex = neigh.ifindex; - } - } - } - } - RouteNetlinkMessage::DelNeighbour(del_neigh) => { - let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet_v4) else { - continue; - }; - if should_skip_neigh(&neigh) { - continue; - } - - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if is_gua_ipv6(addr) && keepalive_gua { - if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { - entries.retain(|e| e.addr != *addr); - debug!("GUA keepalive: removed deleted {} for mac {}", addr, neigh.mac); - } - if private_subnet_v6 { - continue; - } - } - } - - let ip_str = inet_to_string(&neigh.inet); - let key_opt: Option<(String, String)> = leases - .get(&neigh.mac) - .map(|h| (h.clone(), ip_str.clone())) - .or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned()); - - debug!("Del neighbour: {:?}", neigh); - if let Some(key) = key_opt { - registered.remove(&key); - do_delete_dns(&key.0, &neigh.inet, &updater).await; - } - } - _ => {} - } - } - } - _ = probe_timer.tick() => { - if probe_interval == 0 { - continue; - } - probe_registered_neighbours(&prober, ®istered).await; - reconcile_dns( - &updater, - &mut registered, - &leases, - &active_prefixes, - private_subnet_v4, - private_subnet_v6, - &prober, - ) - .await; - - if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await { - let all_dump_ips: std::collections::HashSet = neighbours - .iter() - .map(|n| inet_to_string(&n.inet)) - .collect(); - - for neigh in &neighbours { - if should_skip_neigh(neigh) { - continue; - } - if is_failed_state(neigh.state) { - let ip_str = inet_to_string(&neigh.inet); - let key_opt: Option<(String, String)> = leases - .get(&neigh.mac) - .map(|h| (h.clone(), ip_str.clone())) - .or_else(|| { - registered - .keys() - .find(|(_, ip)| ip == &ip_str) - .cloned() - }); - if let Some(key) = key_opt { - if let Some(entry) = registered.remove(&key) { - debug!("reconcile neigh: kernel FAILED {} -> {:?}", entry.hostname, neigh.inet); - if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await { - registered.insert(key, entry); - } - } - } - continue; - } - if neigh.state != NeighbourState::Reachable { - continue; - } - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if !active_prefixes.is_empty() - && !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)) - { - continue; - } - if is_gua_ipv6(addr) && private_subnet_v6 { - continue; - } - } - let ip_str = inet_to_string(&neigh.inet); - let Some(hostname) = leases.get(&neigh.mac) else { - continue; - }; - let key = (hostname.clone(), ip_str.clone()); - if let Some(entry) = registered.get_mut(&key) { - entry.last_confirmed = Instant::now(); - entry.ifindex = neigh.ifindex; - } else { - debug!("reconcile neigh: kernel orphan {} -> {:?}", hostname, neigh.inet); - try_register_neigh(neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await; - } - } - - let stale_cutoff = Duration::from_secs(probe_interval.saturating_mul(2)); - let now = Instant::now(); - let to_remove: Vec<(String, String)> = registered - .iter() - .filter(|((_hostname, ip_str), entry)| { - !all_dump_ips.contains(ip_str.as_str()) - && now.duration_since(entry.last_confirmed) > stale_cutoff - }) - .map(|(k, _)| k.clone()) - .collect(); - for key in to_remove { - if let Some(entry) = registered.remove(&key) { - let ip_str = &key.1; - let inet = if let Ok(addr) = ip_str.parse::() { - NeighbourAddress::Inet6(addr) - } else if let Ok(addr) = ip_str.parse::() { - NeighbourAddress::Inet(addr) - } else { - registered.insert(key, entry); - continue; - }; - debug!("reconcile neigh: evicted {} -> {}", entry.hostname, ip_str); - if !do_delete_dns(&entry.hostname, &inet, &updater).await { - registered.insert(key, entry); - } - } - } - } - } - _ = gua_keepalive_timer.tick() => { - if !keepalive_gua || keepalive_gua_interval == 0 { - continue; - } - probe_gua_keepalive(&prober, &gua_keepalive, keepalive_gua_per_host); - prune_gua_keepalive(&mut gua_keepalive, keepalive_gua_interval, keepalive_gua_per_host); - } - _ = lease_refresh_timer.tick() => { - match tokio::task::spawn_blocking(|| op::get_lease().map_err(|e| e.to_string())).await { - Ok(Ok(new_leases)) => { - leases = new_leases; - debug!("refreshed {} DHCP leases", leases.len()); - } - Ok(Err(e)) => warn!("failed to refresh DHCP leases from ubus: {}", e), - Err(e) => warn!("DHCP lease refresh task panicked: {}", e), - } - } - } - } - Ok(()) -} diff --git a/ipv6-neigh/src/op.rs b/ipv6-neigh/src/op.rs deleted file mode 100644 index 2782df46..00000000 --- a/ipv6-neigh/src/op.rs +++ /dev/null @@ -1,118 +0,0 @@ -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; - -pub fn call_ubus(obj_path: &str, method: &str) -> Result> { - let socket = Path::new("/var/run/ubus/ubus.sock"); - let mut connection = ubus::Connection::connect(&socket)?; - let json = connection.call(obj_path, method, "")?; - let parsed: Value = serde_json::from_str(&json)?; - Ok(parsed) -} - -/// Decode a hex string (e.g. "44237cdcb75b") to raw bytes and format via -/// the shared `types::format_mac`, producing "44:23:7c:dc:b7:5b". -fn hex_to_mac(hex: &str) -> String { - let bytes: Vec = hex - .as_bytes() - .chunks(2) - .filter_map(|c| u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok()) - .collect(); - crate::types::format_mac(&bytes) -} - -/// Sanitize a DHCP hostname into a valid DNS label. -/// Replaces invalid characters with '-', collapses consecutive hyphens, -/// strips leading/trailing hyphens, and truncates to 63 chars. -fn sanitize_hostname(raw: &str) -> String { - let sanitized: String = raw - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '-' { - c.to_ascii_lowercase() - } else { - '-' - } - }) - .collect(); - // Collapse consecutive hyphens and strip leading/trailing - let collapsed: String = sanitized - .split('-') - .filter(|s| !s.is_empty()) - .collect::>() - .join("-"); - // Truncate to 63 chars (max DNS label length) - collapsed.chars().take(63).collect() -} - -/// Extract a MAC address from a DHCPv6 DUID string. -/// Supports DUID-LLT (type 1) and DUID-LL (type 3) with any hardware type. -/// Returns the colon-separated MAC, e.g. "30:c5:99:7d:d5:ae". -fn mac_from_duid(duid: &str) -> Option { - // All chars must be hex digits - if !duid.chars().all(|c| c.is_ascii_hexdigit()) { - return None; - } - - let duid_type = u16::from_str_radix(duid.get(0..4)?, 16).ok()?; - - // DUID-LLT (1): type(4) + hw_type(4) + time(8) = 16 hex chars before MAC - // DUID-LL (3): type(4) + hw_type(4) = 8 hex chars before MAC - let mac_offset = match duid_type { - 1 => 16, - 3 => 8, - _ => return None, - }; - - let mac_hex = duid.get(mac_offset..mac_offset + 12)?; - Some(hex_to_mac(mac_hex)) -} - -// get mac to hostname mapping -pub fn get_lease() -> Result, Box> { - // dhcpv6 does not use mac address, so we only need to get ipv4 leases - let leases = call_ubus("dhcp", "ipv4leases")?; - let devices = leases["device"].as_object() - .ok_or("missing 'device' in ipv4leases response")?; - - let mut result = HashMap::new(); - - for (_device, leases) in devices { - let leases = leases["leases"].as_array() - .ok_or("missing 'leases' array in device")?; - for lease in leases { - let Some(raw_mac) = lease["mac"].as_str() else { continue }; - let Some(hostname) = lease["hostname"].as_str() else { continue }; - let mac = hex_to_mac(raw_mac); - let hostname = sanitize_hostname(hostname); - if hostname.is_empty() { - continue; - } - // Store bare hostname only; the zone is appended by DnsUpdater - result.insert(mac, hostname); - } - } - - // Supplement with IPv6 leases: extract MAC from DUID when available. - // IPv4 entries take precedence; only fill in missing MACs here. - if let Ok(v6leases) = call_ubus("dhcp", "ipv6leases") { - if let Some(devices) = v6leases["device"].as_object() { - for (_device, device_data) in devices { - let Some(leases) = device_data["leases"].as_array() else { continue }; - for lease in leases { - let Some(duid) = lease["duid"].as_str() else { continue }; - let Some(hostname) = lease["hostname"].as_str() else { continue }; - let hostname = sanitize_hostname(hostname); - if hostname.is_empty() { - continue; - } - let Some(mac) = mac_from_duid(duid) else { continue }; - // IPv4 mapping takes precedence - result.entry(mac).or_insert(hostname); - } - } - } - } - - Ok(result) -} diff --git a/ipv6-neigh/src/probe.rs b/ipv6-neigh/src/probe.rs deleted file mode 100644 index 7c833f51..00000000 --- a/ipv6-neigh/src/probe.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::collections::HashMap; -use std::net::{Ipv4Addr, Ipv6Addr}; -use std::num::NonZeroU32; -use std::time::Instant; - -use log::debug; -use socket2::{Domain, Protocol, SockAddr, Socket, Type}; -use tokio::time::Duration; - -use crate::types::{GuaKeepaliveEntry, RegisteredEntry}; - -fn compute_icmpv4_checksum(packet: &mut [u8]) { - packet[2] = 0; - packet[3] = 0; - let mut sum = 0u32; - for chunk in packet.chunks(2) { - let word = u16::from_be_bytes([chunk[0], *chunk.get(1).unwrap_or(&0)]); - sum = sum.wrapping_add(word as u32); - } - while (sum >> 16) > 0 { - sum = (sum & 0xffff) + (sum >> 16); - } - let checksum = !(sum as u16); - packet[2..4].copy_from_slice(&checksum.to_be_bytes()); -} - -/// Reusable ICMP probe sockets. Created once at startup and shared across all -/// probe call-sites, avoiding the overhead of creating and destroying a raw -/// socket for every single probe packet. -pub(crate) struct Prober { - v6: Socket, - v4: Socket, -} - -impl Prober { - pub fn new() -> std::io::Result { - let v6 = Socket::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?; - v6.set_nonblocking(true)?; - - let v4 = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?; - v4.set_nonblocking(true)?; - - Ok(Self { v6, v4 }) - } - - pub fn send_icmpv6_echo(&self, addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> { - // Re-bind outgoing interface before each send (lightweight setsockopt). - self.v6.bind_device_by_index_v6(NonZeroU32::new(ifindex))?; - - // ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1 - let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1]; - let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0)); - self.v6.send_to(&packet, &dest)?; - Ok(()) - } - - pub fn send_icmpv4_echo(&self, addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> { - // Re-bind outgoing interface before each send (lightweight setsockopt). - self.v4.bind_device_by_index_v4(NonZeroU32::new(ifindex))?; - - // ICMPv4 Echo Request: type=8, code=0, checksum (computed), id=0, seq=1 - let mut packet: [u8; 8] = [8, 0, 0, 0, 0, 0, 0, 1]; - compute_icmpv4_checksum(&mut packet); - let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0)); - self.v4.send_to(&packet, &dest)?; - Ok(()) - } -} - -/// Send ICMPv6/ICMPv4 Echo Requests to all registered neighbours that haven't been -/// confirmed recently. This forces the kernel NUD state machine to verify reachability, -/// generating NewNeighbour events with the resulting state (Reachable or Failed). -pub(crate) async fn probe_registered_neighbours( - prober: &Prober, - registered: &HashMap<(String, String), RegisteredEntry>, -) { - let now = Instant::now(); - for ((_, ip_str), entry) in registered.iter() { - // Only probe entries not confirmed recently (older than 30s) - if now.duration_since(entry.last_confirmed) < Duration::from_secs(30) { - continue; - } - if let Ok(addr) = ip_str.parse::() { - if let Err(e) = prober.send_icmpv6_echo(addr, entry.ifindex) { - debug!("probe failed for {}: {}", ip_str, e); - } - } else if let Ok(addr) = ip_str.parse::() { - if let Err(e) = prober.send_icmpv4_echo(addr, entry.ifindex) { - debug!("probe failed for {}: {}", ip_str, e); - } - } - } -} - -/// Send ICMPv6 Echo Requests to the newest GUA addresses per host for NUD keepalive. -/// Only the top `per_host` addresses (by most-recently-seen) are probed per MAC. -/// This does NOT publish any records to DNS. -pub(crate) fn probe_gua_keepalive( - prober: &Prober, - gua_keepalive: &HashMap>, - per_host: usize, -) { - let now = Instant::now(); - for (mac, entries) in gua_keepalive.iter() { - // Sort indices by first_seen descending to pick the newest GUAs. - let mut indices: Vec = (0..entries.len()).collect(); - indices.sort_by(|a, b| entries[*b].first_seen.cmp(&entries[*a].first_seen)); - - for &idx in indices.iter().take(per_host) { - let e = &entries[idx]; - // Skip entries confirmed recently (within 30s) - if now.duration_since(e.last_confirmed) < Duration::from_secs(30) { - continue; - } - match prober.send_icmpv6_echo(e.addr, e.ifindex) { - Ok(()) => debug!("GUA keepalive probe: {} ({}) -> {}", e.hostname, mac, e.addr), - Err(err) => debug!("GUA keepalive probe failed for {}: {}", e.addr, err), - } - } - } -} - -/// Remove GUA keepalive entries that haven't been confirmed REACHABLE within 3x the -/// keepalive interval, and drop excess entries beyond `per_host` (oldest first). -pub(crate) fn prune_gua_keepalive( - gua_keepalive: &mut HashMap>, - keepalive_interval: u64, - per_host: usize, -) { - let timeout = Duration::from_secs(keepalive_interval.saturating_mul(3)); - let now = Instant::now(); - gua_keepalive.retain(|_, entries| { - // Remove timed-out entries first. - entries.retain(|e| now.duration_since(e.last_confirmed) < timeout); - // Then keep only the newest `per_host` entries (by first_seen desc). - if entries.len() > per_host { - entries.sort_by(|a, b| b.first_seen.cmp(&a.first_seen)); - entries.truncate(per_host); - } - !entries.is_empty() - }); -} diff --git a/ipv6-neigh/src/reconcile.rs b/ipv6-neigh/src/reconcile.rs deleted file mode 100644 index 834d4716..00000000 --- a/ipv6-neigh/src/reconcile.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::collections::HashMap; -use std::net::{IpAddr, Ipv6Addr}; -use std::time::Instant; - -use log::{debug, error, info, warn}; -use netlink_packet_route::neighbour::NeighbourAddress; - -use crate::db::DnsUpdater; -use crate::filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, is_gua_ipv6}; -use crate::probe::Prober; -use crate::types::{LanPrefix, Neigh, RegisteredEntry, DEFAULT_TTL}; - -pub(crate) async fn process_new_neigh( - neigh: &Neigh, - updater: &DnsUpdater, - leases: &HashMap, - private_subnet_v6: bool, -) -> bool { - let Some(hostname) = leases.get(&neigh.mac) else { - debug!("no lease for mac {}, skipping DNS update", neigh.mac); - return false; - }; - - // Guard: never publish GUA to DNS when private_subnet_v6 is set. - if let NeighbourAddress::Inet6(addr) = &neigh.inet { - if private_subnet_v6 && is_gua_ipv6(addr) { - debug!("skipping GUA DNS publish for {} (private_subnet_v6)", hostname); - return false; - } - } - - let result = match &neigh.inet { - NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await, - NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await, - _ => return false, - }; - - match result { - Ok(()) => { - info!("DNS update: added {} -> {:?}", hostname, neigh.inet); - let ip = match &neigh.inet { - NeighbourAddress::Inet6(addr) => IpAddr::V6(*addr), - NeighbourAddress::Inet(addr) => IpAddr::V4(*addr), - _ => return true, - }; - if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await { - warn!("PTR update failed for {} ({}): {}", hostname, ip, e); - } - true - } - Err(e) => { - error!("DNS update failed for {}: {}", hostname, e); - false - } - } -} - -/// Delete a specific DNS record directly by hostname and address. -pub(crate) async fn do_delete_dns( - hostname: &str, - inet: &NeighbourAddress, - updater: &DnsUpdater, -) -> bool { - let result = match inet { - NeighbourAddress::Inet6(addr) => updater.delete_aaaa(hostname, *addr).await, - NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await, - _ => return true, - }; - - match result { - Ok(()) => { - info!("DNS update: removed {} -> {:?}", hostname, inet); - let ip = match inet { - NeighbourAddress::Inet6(addr) => IpAddr::V6(*addr), - NeighbourAddress::Inet(addr) => IpAddr::V4(*addr), - _ => return true, - }; - if let Err(e) = updater.delete_ptr(ip).await { - warn!("PTR delete failed for {} ({}): {}", hostname, ip, e); - } - true - } - Err(e) => { - error!("DNS delete failed for {}: {}", hostname, e); - false - } - } -} - -/// Enforce the per-host ULA AAAA limit. If `host` already has `max` or more ULA AAAA -/// records in `registered`, remove the oldest (by `last_confirmed`) and delete from DNS. -/// Returns the number of existing ULA AAAA records for this host (after pruning). -pub(crate) async fn prune_ula_for_host( - host: &str, - max: usize, - registered: &mut HashMap<(String, String), RegisteredEntry>, - updater: &DnsUpdater, -) -> usize { - // Collect existing ULA AAAA keys for this host. - let mut ula_keys: Vec<((String, String), Instant)> = registered - .iter() - .filter(|((h, ip_str), _)| { - h == host && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) - }) - .map(|(k, e)| (k.clone(), e.last_confirmed)) - .collect(); - - let initial_count = ula_keys.len(); - if initial_count < max { - return initial_count; - } - - // Sort oldest first (by last_confirmed asc). - ula_keys.sort_by_key(|(_, ts)| *ts); - - // Remove excess (keep the newest `max - 1` so there's room for the new one). - let to_remove = initial_count - (max.saturating_sub(1)); - let mut actually_removed = 0; - for (key, _) in ula_keys.into_iter().take(to_remove) { - if let Some(entry) = registered.remove(&key) { - let addr: Ipv6Addr = key.1.parse().unwrap(); - let inet = NeighbourAddress::Inet6(addr); - do_delete_dns(&entry.hostname, &inet, updater).await; - info!("ULA pruning: removed oldest {} -> {} for host {}", entry.hostname, key.1, host); - actually_removed += 1; - } - } - - // Return count after pruning (computed, no second scan needed). - initial_count - actually_removed -} - -/// Reconcile the in-memory `registered` map against the live DNS zone obtained via AXFR. -/// -/// Two corrections are made on each call: -/// 1. **DNS orphans** -- records present in DNS but absent from `registered`. -/// These are either stale leftovers from a previous run or records from a prefix that -/// is no longer active. Records that fail prefix/subnet filtering are deleted from DNS -/// immediately; records that pass filtering are probed with ICMP so the kernel NUD -/// state machine can confirm reachability and re-populate `registered` via the event loop. -/// 2. **Registered orphans** -- entries in `registered` that are missing from DNS (e.g. -/// because hickory-dns restarted and lost its in-memory state). These are re-pushed -/// via DNS UPDATE so the zone stays consistent. -pub(crate) async fn reconcile_dns( - updater: &DnsUpdater, - registered: &mut HashMap<(String, String), RegisteredEntry>, - leases: &HashMap, - active_prefixes: &[LanPrefix], - private_subnet_v4: bool, - private_subnet_v6: bool, - prober: &Prober, -) { - use std::collections::{HashMap as Map, HashSet}; - - let dns_records = match updater.axfr_records().await { - Ok(r) => r, - Err(e) => { - warn!("AXFR reconciliation failed: {}", e); - return; - } - }; - - // Only consider records whose hostname appears in the lease table; this avoids - // accidentally touching manually-added records or the router's own addresses. - let lease_hostnames: HashSet<&str> = leases.values().map(String::as_str).collect(); - - // Map: ip_string -> hostname, for DNS records that pass all filters. - // Records that fail filtering are deleted from DNS here. - let mut dns_ips: Map = Map::new(); - - for (hostname, ip) in &dns_records { - if !lease_hostnames.contains(hostname.as_str()) { - continue; - } - let passes = match ip { - IpAddr::V6(addr) => { - let subnet_ok = !private_subnet_v6 || if_ipv6_in_private_subnet(addr); - let prefix_ok = active_prefixes.is_empty() - || active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)); - subnet_ok && prefix_ok - } - IpAddr::V4(addr) => !private_subnet_v4 || if_ipv4_in_private_subnet(addr), - }; - - if passes { - dns_ips.insert(ip.to_string(), hostname.clone()); - } else { - // Stale record (wrong prefix / subnet) -- remove from DNS. - let result = match ip { - IpAddr::V6(addr) => updater.delete_aaaa(hostname, *addr).await, - IpAddr::V4(addr) => updater.delete_a(hostname, *addr).await, - }; - match result { - Ok(()) => { - info!("reconcile: deleted stale DNS {} -> {}", hostname, ip); - if let Err(e) = updater.delete_ptr(*ip).await { - warn!("reconcile: PTR delete failed for stale {} {}: {}", hostname, ip, e); - } - } - Err(e) => warn!("reconcile: failed to delete stale DNS {} {}: {}", hostname, ip, e), - } - } - } - - // Registered ip set for quick lookup. - let registered_ips: HashSet<&str> = - registered.keys().map(|(_, ip)| ip.as_str()).collect(); - - // --- DNS orphans (in DNS but not in registered) --- - for (ip_str, hostname) in &dns_ips { - if registered_ips.contains(ip_str.as_str()) { - continue; - } - info!("reconcile: DNS orphan {} -> {}, deleting and probing", hostname, ip_str); - - let del_result = match ip_str.parse::() { - Ok(IpAddr::V6(addr)) => updater.delete_aaaa(hostname, addr).await, - Ok(IpAddr::V4(addr)) => updater.delete_a(hostname, addr).await, - _ => continue, - }; - match del_result { - Ok(()) => { - info!("reconcile: deleted orphan DNS {} -> {}", hostname, ip_str); - if let Ok(ip) = ip_str.parse::() { - if let Err(e) = updater.delete_ptr(ip).await { - warn!("reconcile: PTR delete failed for orphan {} {}: {}", hostname, ip_str, e); - } - } - } - Err(e) => warn!("reconcile: failed to delete orphan {} {}: {}", hostname, ip_str, e), - } - - // Probe so alive devices trigger a REACHABLE event and re-register. - match ip_str.parse::() { - Ok(IpAddr::V6(addr)) => { - if let Err(e) = prober.send_icmpv6_echo(addr, 0) { - debug!("reconcile: probe failed for {}: {}", addr, e); - } - } - Ok(IpAddr::V4(addr)) => { - if let Err(e) = prober.send_icmpv4_echo(addr, 0) { - debug!("reconcile: probe failed for {}: {}", addr, e); - } - } - _ => {} - } - } - - // --- Registered orphans (in registered but not in DNS) --- - for ((_, ip_str), entry) in registered.iter_mut() { - if dns_ips.contains_key(ip_str.as_str()) { - continue; - } - let hostname = &entry.hostname; - debug!("reconcile: registered orphan {} -> {}, re-pushing", hostname, ip_str); - - let result = match ip_str.parse::() { - Ok(IpAddr::V6(addr)) => updater.upsert_aaaa(hostname, addr, DEFAULT_TTL).await, - Ok(IpAddr::V4(addr)) => updater.upsert_a(hostname, addr, DEFAULT_TTL).await, - _ => continue, - }; - match result { - Ok(()) => { - info!("reconcile: re-pushed {} -> {}", hostname, ip_str); - if let Ok(ip) = ip_str.parse::() { - if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await { - warn!("reconcile: PTR re-push failed for {} {}: {}", hostname, ip_str, e); - } - } - entry.last_confirmed = Instant::now(); - } - Err(e) => warn!("reconcile: failed to re-push {} {}: {}", hostname, ip_str, e), - } - } -} diff --git a/ipv6-neigh/src/types.rs b/ipv6-neigh/src/types.rs deleted file mode 100644 index 1ef0a8b5..00000000 --- a/ipv6-neigh/src/types.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::fmt::Write; -use std::net::Ipv6Addr; -use std::time::Instant; -use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourState}; -use netlink_packet_route::route::RouteType; - -pub(crate) const RTNLGRP_NEIGH: u32 = 3; -pub(crate) const DEFAULT_TTL: u32 = 60; - -pub(crate) const fn nl_mgrp(group: u32) -> u32 { - if group > 31 { - panic!("use netlink_sys::Socket::add_membership() for this group"); - } - if group == 0 { 0 } else { 1 << (group - 1) } -} - -/// Format raw MAC bytes as colon-separated hex string, e.g. "44:23:7c:dc:b7:5b". -/// Shared across modules to avoid duplicate formatting logic. -pub(crate) fn format_mac(mac: &[u8]) -> String { - let mut s = String::with_capacity(mac.len() * 3); - for (i, byte) in mac.iter().enumerate() { - if i > 0 { s.push(':'); } - write!(s, "{:02x}", byte).unwrap(); - } - s -} - -#[derive(Debug)] -pub(crate) struct Neigh { - pub(crate) ifindex: u32, - pub(crate) state: NeighbourState, - pub(crate) kind: RouteType, - pub(crate) inet: NeighbourAddress, - pub(crate) mac: String, -} - -/// An IPv6 prefix (address + length) representing an active LAN prefix on the router interface. -pub(crate) struct LanPrefix { - pub(crate) addr: Ipv6Addr, - pub(crate) prefix_len: u8, -} - -/// State tracked for each (mac, ip) pair that has been successfully registered in DNS. -pub(crate) struct RegisteredEntry { - pub(crate) hostname: String, - pub(crate) last_confirmed: Instant, - pub(crate) ifindex: u32, -} - -/// A GUA address tracked for keepalive probing only (not published to DNS). -pub(crate) struct GuaKeepaliveEntry { - pub(crate) hostname: String, - pub(crate) addr: Ipv6Addr, - pub(crate) ifindex: u32, - /// When this GUA was first observed -- used to select the "newest" address per host. - pub(crate) first_seen: Instant, - /// Last time this address was confirmed REACHABLE by the kernel. - pub(crate) last_confirmed: Instant, -} diff --git a/luci-theme-material3/htdocs/luci-static/material3/mobile.css b/luci-theme-material3/htdocs/luci-static/material3/mobile.css index 025a08a7..2e7ebf8b 100644 --- a/luci-theme-material3/htdocs/luci-static/material3/mobile.css +++ b/luci-theme-material3/htdocs/luci-static/material3/mobile.css @@ -263,14 +263,14 @@ header h3 a { font-weight: bold; } - .cbi-value[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) { + .cbi-value:not(.hidden)[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) { display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; } - .cbi-value[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > label.cbi-value-title { + .cbi-value:not(.hidden)[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > label.cbi-value-title { width: auto; max-width: none; padding: 0; @@ -280,15 +280,15 @@ header h3 a { white-space: normal; } - .cbi-value[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field { + .cbi-value:not(.hidden)[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field { display: contents; } - .cbi-value[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-checkbox { + .cbi-value:not(.hidden)[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-checkbox { grid-column: 2; } - .cbi-value[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-value-description { + .cbi-value:not(.hidden)[data-widget="CBI.FlagValue"]:has(> .cbi-value-field > .cbi-checkbox) > .cbi-value-field > .cbi-value-description { grid-column: 1 / -1; width: auto; max-width: calc(100% - 1.25em); @@ -296,6 +296,10 @@ header h3 a { margin: .25em 0 0 1.25em; } + .cbi-value.hidden { + display: none !important; + } + .cbi-value-field, .cbi-select, .cbi-dynlist,