diff --git a/haproxy/Makefile b/haproxy/Makefile new file mode 100644 index 00000000..7b89e548 --- /dev/null +++ b/haproxy/Makefile @@ -0,0 +1,131 @@ +# +# Copyright (C) 2010-2016 OpenWrt.org +# Copyright (C) 2009-2016 Thomas Heil +# Copyright (C) 2018 Christian Lachner +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=haproxy +PKG_VERSION:=3.2.19 +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://www.haproxy.org/download/3.2/src +PKG_HASH:=skip + +PKG_MAINTAINER:=Thomas Heil , \ + Christian Lachner +PKG_LICENSE:=GPL-2.0-only +PKG_LICENSE_FILES:=LICENSE +PKG_CPE_ID:=cpe:/a:haproxy:haproxy + +include $(INCLUDE_DIR)/package.mk + +define Package/haproxy/Default + SUBMENU:=Web Servers/Proxies + SECTION:=net + CATEGORY:=Network + TITLE:=TCP/HTTP Load Balancer + URL:=https://www.haproxy.org/ + DEPENDS:= +USE_GLIBC:libcrypt-compat +libpcre2 +libltdl +zlib +libpthread +liblua5.4 +libatomic +endef + +define Package/haproxy/Default/description + Open source Reliable, High Performance TCP/HTTP Load Balancer. +endef + +define Package/haproxy + $(call Package/haproxy/Default) + TITLE+=with SSL support + DEPENDS+= +libopenssl +libncurses +libreadline + VARIANT:=ssl +endef + +define Package/haproxy/description +$(call Package/haproxy/Default/description) + This package is built with SSL and LUA support. +endef + +define Package/haproxy-nossl + $(call Package/haproxy/Default) + TITLE+=without SSL support + VARIANT:=nossl + CONFLICTS:=haproxy +endef + +define Package/haproxy-nossl/description + $(call Package/haproxy/Default/description) + This package is built without SSL support. +endef + +TARGET=linux-musl +ENABLE_LUA:=y + +ifeq ($(CONFIG_USE_GLIBC),y) + TARGET=linux-glibc +endif + +ifeq ($(BUILD_VARIANT),ssl) + ADDON+=USE_OPENSSL=1 + ADDON+=ADDLIB="-lcrypto -lm" + ADDON+=USE_QUIC=1 +endif + +define Build/Compile + $(MAKE) TARGET=$(TARGET) -C $(PKG_BUILD_DIR) \ + DESTDIR="$(PKG_INSTALL_DIR)" \ + CC="$(TARGET_CC)" \ + PCREDIR="$(STAGING_DIR)/usr/" \ + USE_LUA=1 LUA_LIB_NAME="lua5.4" LUA_INC="$(STAGING_DIR)/usr/include/lua5.4" LUA_LIB="$(STAGING_DIR)/usr/lib" \ + USE_ZLIB=1 USE_PCRE2=1 USE_PCRE2_JIT=1 USE_LIBATOMIC=1 USE_PROMEX=1 \ + VERSION="$(PKG_VERSION)" SUBVERS="-$(PKG_RELEASE)" \ + VERDATE="$(shell date -d @$(SOURCE_DATE_EPOCH) '+%Y/%m/%d')" IGNOREGIT=1 \ + $(ADDON) \ + CFLAGS="$(TARGET_CFLAGS) -fno-strict-aliasing -Wdeclaration-after-statement -Wno-unused-label -Wno-sign-compare -Wno-unused-parameter -Wno-clobbered -Wno-missing-field-initializers -Wno-cast-function-type -Wno-address-of-packed-member -Wtype-limits -Wshift-negative-value -Wshift-overflow=2 -Wduplicated-cond -Wnull-dereference -fwrapv -fasynchronous-unwind-tables -Wno-null-dereference" \ + LD="$(TARGET_CC)" \ + LDFLAGS="$(TARGET_LDFLAGS)" + + $(MAKE_VARS) $(MAKE) -C $(PKG_BUILD_DIR) \ + DESTDIR="$(PKG_INSTALL_DIR)" \ + LD="$(TARGET_CC)" \ + LDFLAGS="$(TARGET_LDFLAGS)" \ + $(MAKE_FLAGS) \ + install + + $(MAKE_VARS) $(MAKE) -C $(PKG_BUILD_DIR) \ + DESTDIR="$(PKG_INSTALL_DIR)" \ + CC="$(TARGET_CC)" \ + CFLAGS="$(TARGET_CFLAGS) -Wno-address-of-packed-member" \ + LDFLAGS="$(TARGET_LDFLAGS)" \ + admin/halog/halog +endef + +define Package/haproxy/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/haproxy $(1)/usr/sbin/ +endef + +Package/haproxy-nossl/install = $(Package/haproxy/install) + +define Package/halog + $(call Package/haproxy) + TITLE+=halog + DEPENDS:=haproxy +endef + +define Package/halog/description + HAProxy Log Analyzer +endef + +define Package/halog/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/admin/halog/halog $(1)/usr/bin/ +endef + +$(eval $(call BuildPackage,haproxy)) +$(eval $(call BuildPackage,halog)) +$(eval $(call BuildPackage,haproxy-nossl)) diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index 6a9e9893..61ee8aa2 100644 --- a/ipv6-neigh/Makefile +++ b/ipv6-neigh/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ipv6-neigh PKG_VERSION:=0.1.0 -PKG_RELEASE:=10 +PKG_RELEASE:=11 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 diff --git a/ipv6-neigh/src/filter.rs b/ipv6-neigh/src/filter.rs new file mode 100644 index 00000000..23bcd7a5 --- /dev/null +++ b/ipv6-neigh/src/filter.rs @@ -0,0 +1,58 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::types::LanPrefix; + +pub(crate) fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool { + // Check if address is ULA (fc00::/7) + // Note: link-local is always filtered earlier by is_link_local_ipv6() + (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 +} + +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 new file mode 100644 index 00000000..7303bc8a --- /dev/null +++ b/ipv6-neigh/src/iface.rs @@ -0,0 +1,145 @@ +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::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 +} + +/// 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 mut links = handle.link().get().match_name(iface.to_owned()).execute(); + let link = match links.try_next().await { + Ok(Some(l)) => l, + _ => return vec![], + }; + let ifindex = link.header.index; + let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute(); + let mut prefixes = Vec::new(); + while let Ok(Some(msg)) = addresses.try_next().await { + let prefix_len = msg.header.prefix_len; + // Skip deprecated addresses; they are no longer used for new connections + // and should not be treated as active LAN prefixes. + if is_addr_deprecated(&msg) { + continue; + } + for attr in &msg.attributes { + if let AddressAttribute::Address(IpAddr::V6(addr)) = attr { + // Skip link-local + if (addr.segments()[0] & 0xffc0) == 0xfe80 { + continue; + } + let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00; + if private_subnet_v6 && !is_ula { + 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 mut links = handle.link().get().match_name(iface.to_owned()).execute(); + let link = match links.try_next().await { + Ok(Some(l)) => l, + Ok(None) => { + warn!("interface {} not found, skipping router address registration", iface); + return; + } + Err(e) => { + warn!("failed to find interface {}: {}", iface, e); + return; + } + }; + let ifindex = link.header.index; + let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute(); + while let Ok(Some(msg)) = addresses.try_next().await { + 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) => { + // Always skip link-local + if (addr.segments()[0] & 0xffc0) == 0xfe80 { + continue; + } + // Restrict to ULA only when private_subnet_v6 is set + let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00; + if private_subnet_v6 && !is_ula { + 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), + } + } + } + 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), + } + } + } + } + } + } +} + +/// 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 index 66a289f0..89e0bbc7 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr}; +use std::net::SocketAddr as StdSocketAddr; use std::time::Instant; use futures::stream::StreamExt; @@ -7,13 +7,10 @@ use futures::stream::TryStreamExt; use log::{debug, error, info, warn}; use netlink_packet_core::NetlinkPayload; use netlink_packet_route::RouteNetlinkMessage; -use netlink_packet_route::address::{AddressAttribute, AddressFlags, AddressHeaderFlags, AddressMessage}; 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 socket2::{Domain, Protocol, SockAddr, Socket, Type}; -use std::num::NonZeroU32; use tokio::time::{self, Duration}; use clap::Parser; @@ -21,18 +18,22 @@ 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; -const RTNLGRP_NEIGH: u32 = 3; +mod probe; +mod reconcile; +mod types; -const DEFAULT_TTL: u32 = 60; - -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) } -} +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::{probe_gua_keepalive, probe_registered_neighbours, prune_gua_keepalive, send_icmpv4_echo, send_icmpv6_echo}; +use reconcile::{do_delete_dns, process_new_neigh, prune_ula_for_mac, reconcile_dns}; +use types::{ + DEFAULT_TTL, RTNLGRP_NEIGH, GuaKeepaliveEntry, Neigh, RegisteredEntry, nl_mgrp, +}; #[derive(Debug, Parser)] #[clap()] @@ -74,149 +75,13 @@ struct Cli { #[clap(long, default_value = "120")] keepalive_gua_interval: u64, /// Maximum number of GUA addresses to keepalive-probe per host - #[clap(long, default_value = "1")] + #[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, } -#[derive(Debug)] -struct Neigh { - ifindex: u32, - state: NeighbourState, - kind: RouteType, - inet: NeighbourAddress, - mac: String, -} - -/// An IPv6 prefix (address + length) representing an active LAN prefix on the router interface. -struct LanPrefix { - addr: Ipv6Addr, - prefix_len: u8, -} - -/// State tracked for each (mac, ip) pair that has been successfully registered in DNS. -struct RegisteredEntry { - hostname: String, - last_confirmed: Instant, - ifindex: u32, -} - -/// A GUA address tracked for keepalive probing only (not published to DNS). -struct GuaKeepaliveEntry { - hostname: String, - addr: Ipv6Addr, - ifindex: u32, - /// When this GUA was first observed — used to select the "newest" address per host. - first_seen: Instant, - /// Last time this address was confirmed REACHABLE by the kernel. - last_confirmed: Instant, -} - -fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool { - // Check if address is ULA (fc00::/7) - // Note: link-local is always filtered earlier by is_link_local_ipv6() - (ip.segments()[0] & 0xfe00) == 0xfc00 -} - -/// Returns true if `addr` is a Global Unicast Address (2000::/3). -fn is_gua_ipv6(addr: &Ipv6Addr) -> bool { - (addr.segments()[0] & 0xe000) == 0x2000 -} - -fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool { - // Check for private network ranges - 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. -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). -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)) -} - -async fn process_new_neigh(neigh: &Neigh, updater: &db::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); - true - } - Err(e) => { - error!("DNS update failed for {}: {}", hostname, e); - false - } - } -} - -/// Delete a specific DNS record directly by hostname and address. -async fn do_delete_dns(hostname: &str, inet: &NeighbourAddress, updater: &db::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); - true - } - Err(e) => { - error!("DNS delete failed for {}: {}", hostname, e); - false - } - } -} fn should_skip_route_type(route_type: RouteType) -> bool { matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local) @@ -231,52 +96,7 @@ fn is_failed_state(state: NeighbourState) -> bool { matches!(state, NeighbourState::Failed) } -/// Enforce the per-host ULA AAAA limit. If `mac` 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 MAC (after pruning). -async fn prune_ula_for_mac( - mac: &str, - max: usize, - registered: &mut HashMap<(String, String), RegisteredEntry>, - updater: &db::DnsUpdater, -) -> usize { - // Collect existing ULA AAAA keys for this MAC. - let mut ula_keys: Vec<((String, String), Instant)> = registered - .iter() - .filter(|((m, ip_str), _)| { - m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) - }) - .map(|(k, e)| (k.clone(), e.last_confirmed)) - .collect(); - - if ula_keys.len() < max { - return ula_keys.len(); - } - - // 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 = ula_keys.len() - (max.saturating_sub(1)); - 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 mac {}", entry.hostname, key.1, mac); - } - } - - // Return count after pruning. - registered - .iter() - .filter(|((m, ip_str), _)| { - m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) - }) - .count() -} - -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), ()> { let args = Cli::parse(); @@ -311,7 +131,14 @@ async fn main() -> Result<(), ()> { // Wait for the DNS server to become available before sending any updates. wait_for_dns_server(args.dns_server).await; - let (connection, handle, _) = new_connection().unwrap(); + let (mut connection, handle, mut messages) = new_connection().unwrap(); + // Subscribe to neighbour events on the same connection used for queries. + // This avoids a second netlink socket and spawned task. + 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 @@ -334,8 +161,13 @@ async fn main() -> Result<(), ()> { .await; } - // Load DHCP leases (mac -> hostname) from ubus - let mut leases = op::get_lease().unwrap_or_default(); + // Load DHCP leases (mac -> hostname) from ubus. + // Use spawn_blocking: op::get_lease() makes a synchronous ubus call. + let mut leases: HashMap = tokio::task::spawn_blocking(|| { + op::get_lease().unwrap_or_default() + }) + .await + .unwrap_or_default(); info!("loaded {} DHCP leases", leases.len()); // State cache: tracks (mac, ip_string) -> registered entry (hostname, last confirmed time, ifindex) @@ -480,18 +312,7 @@ async fn main() -> Result<(), ()> { } } - let (mut conn, mut _handle, mut messages) = - new_connection().map_err(|e| format!("{e}")).unwrap(); - - let groups = nl_mgrp(RTNLGRP_NEIGH); - - let addr = SocketAddr::new(0, groups); - conn.socket_mut() - .socket_mut() - .bind(&addr) - .expect("Failed to bind"); - - tokio::spawn(conn); + // (netlink event socket already set up above — reuse the same connection) // Start periodic probing task let probe_interval = args.probe_interval; @@ -705,254 +526,6 @@ async fn main() -> Result<(), ()> { Ok(()) } -/// 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. -async fn reconcile_dns( - updater: &db::DnsUpdater, - registered: &mut HashMap<(String, String), RegisteredEntry>, - leases: &HashMap, - active_prefixes: &[LanPrefix], - private_subnet_v4: bool, - private_subnet_v6: bool, -) { - use std::collections::{HashMap as Map, HashSet}; - use std::net::IpAddr; - - 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), - 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) --- - // Delete the orphan immediately — DNS TTL only controls resolver caches, not - // authoritative record lifetime, so records do NOT auto-expire. - // After deletion we probe the address: if the device is still alive the kernel - // will emit a REACHABLE NewNeighbour event which re-registers it in DNS. - 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), - 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) = send_icmpv6_echo(addr, 0) { - debug!("reconcile: probe failed for {}: {}", addr, e); - } - } - Ok(IpAddr::V4(addr)) => { - if let Err(e) = send_icmpv4_echo(addr, 0) { - debug!("reconcile: probe failed for {}: {}", addr, e); - } - } - _ => {} - } - } - - // --- Registered orphans (in registered but not in DNS) --- - // hickory-dns may have restarted and lost its journal; re-push the record. - // Use the hostname stored in the entry — independent of the current lease table. - 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); - entry.last_confirmed = Instant::now(); - } - Err(e) => warn!("reconcile: failed to re-push {} {}: {}", hostname, ip_str, e), - } - } -} - -/// Send ICMPv6 Echo Request / ICMPv4 Echo Request to all registered neighbours. -/// This forces the kernel NUD state machine to verify reachability, generating -/// NewNeighbour events with the resulting state (Reachable or Failed). -async fn probe_registered_neighbours(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) = 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) = send_icmpv4_echo(addr, entry.ifindex) { - debug!("probe failed for {}: {}", ip_str, e); - } - } - } -} - -/// Send ICMPv6 Echo Request 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. -fn probe_gua_keepalive( - 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 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 3× the -/// keepalive interval, and drop excess entries beyond `per_host` (oldest first). -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() - }); -} - -fn set_ipv6_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { - socket.bind_device_by_index_v6(NonZeroU32::new(ifindex)) -} - -fn set_ip_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { - socket.bind_device_by_index_v4(NonZeroU32::new(ifindex)) -} - -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()); -} - -fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> { - let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::ICMPV6))?; - socket.set_nonblocking(true)?; - // Bind outgoing packet to the specific interface via IPV6_UNICAST_IF so the - // kernel NUD state machine updates the correct neighbour entry. - set_ipv6_unicast_if(&socket, 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)); - socket.send_to(&packet, &dest)?; - Ok(()) -} - -fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> { - let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?; - socket.set_nonblocking(true)?; - // Bind outgoing packet to the specific interface via IP_UNICAST_IF. - set_ip_unicast_if(&socket, 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)); - socket.send_to(&packet, &dest)?; - Ok(()) -} fn format_mac(mac: Vec) -> String { let mut mac_str = String::new(); @@ -1028,133 +601,3 @@ async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result 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 -} - -/// Enumerate non-link-local IPv6 prefixes currently assigned to `iface`. -/// Used during startup to filter out neighbour entries from expired/old prefixes. -async fn get_active_lan_prefixes(handle: Handle, iface: &str, private_subnet_v6: bool) -> Vec { - let mut links = handle.link().get().match_name(iface.to_owned()).execute(); - let link = match links.try_next().await { - Ok(Some(l)) => l, - _ => return vec![], - }; - let ifindex = link.header.index; - let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute(); - let mut prefixes = Vec::new(); - while let Ok(Some(msg)) = addresses.try_next().await { - let prefix_len = msg.header.prefix_len; - // Skip deprecated addresses; they are no longer used for new connections - // and should not be treated as active LAN prefixes. - if is_addr_deprecated(&msg) { - continue; - } - for attr in &msg.attributes { - if let AddressAttribute::Address(IpAddr::V6(addr)) = attr { - // Skip link-local - if (addr.segments()[0] & 0xffc0) == 0xfe80 { - continue; - } - let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00; - if private_subnet_v6 && !is_ula { - continue; - } - prefixes.push(LanPrefix { addr: *addr, prefix_len }); - } - } - } - prefixes -} - -/// Enumerate addresses on `iface`, register A/AAAA records for the router itself. -async fn register_router_addresses( - handle: Handle, - iface: &str, - hostname: &str, - extra_alias: Option<&str>, - updater: &db::DnsUpdater, - private_subnet_v6: bool, -) { - 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) => { - warn!("interface {} not found, skipping router address registration", iface); - return; - } - Err(e) => { - warn!("failed to find interface {}: {}", iface, e); - return; - } - }; - let ifindex = link.header.index; - let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute(); - while let Ok(Some(msg)) = addresses.try_next().await { - 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) => { - // Always skip link-local - if (addr.segments()[0] & 0xffc0) == 0xfe80 { - continue; - } - // Restrict to ULA only when private_subnet_v6 is set - let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00; - if private_subnet_v6 && !is_ula { - 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), - } - } - } - 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), - } - } - } - } - } - } -} - -/// 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. -async fn wait_for_dns_server(addr: StdSocketAddr) { - 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/probe.rs b/ipv6-neigh/src/probe.rs new file mode 100644 index 00000000..df519d86 --- /dev/null +++ b/ipv6-neigh/src/probe.rs @@ -0,0 +1,131 @@ +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 set_ipv6_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { + socket.bind_device_by_index_v6(NonZeroU32::new(ifindex)) +} + +fn set_ip_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { + socket.bind_device_by_index_v4(NonZeroU32::new(ifindex)) +} + +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()); +} + +pub(crate) fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::ICMPV6))?; + socket.set_nonblocking(true)?; + // Bind outgoing packet to the specific interface via IPV6_UNICAST_IF so the + // kernel NUD state machine updates the correct neighbour entry. + set_ipv6_unicast_if(&socket, 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)); + socket.send_to(&packet, &dest)?; + Ok(()) +} + +pub(crate) fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?; + socket.set_nonblocking(true)?; + // Bind outgoing packet to the specific interface via IP_UNICAST_IF. + set_ip_unicast_if(&socket, 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)); + socket.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( + 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) = 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) = 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( + 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 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 3× 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 new file mode 100644 index 00000000..1080cd5b --- /dev/null +++ b/ipv6-neigh/src/reconcile.rs @@ -0,0 +1,242 @@ +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::{send_icmpv4_echo, send_icmpv6_echo}; +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); + 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); + true + } + Err(e) => { + error!("DNS delete failed for {}: {}", hostname, e); + false + } + } +} + +/// Enforce the per-host ULA AAAA limit. If `mac` 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 MAC (after pruning). +pub(crate) async fn prune_ula_for_mac( + mac: &str, + max: usize, + registered: &mut HashMap<(String, String), RegisteredEntry>, + updater: &DnsUpdater, +) -> usize { + // Collect existing ULA AAAA keys for this MAC. + let mut ula_keys: Vec<((String, String), Instant)> = registered + .iter() + .filter(|((m, ip_str), _)| { + m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) + }) + .map(|(k, e)| (k.clone(), e.last_confirmed)) + .collect(); + + if ula_keys.len() < max { + return ula_keys.len(); + } + + // 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 = ula_keys.len() - (max.saturating_sub(1)); + 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 mac {}", entry.hostname, key.1, mac); + } + } + + // Return count after pruning. + registered + .iter() + .filter(|((m, ip_str), _)| { + m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) + }) + .count() +} + +/// 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, +) { + 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), + 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) --- + // Delete the orphan immediately — DNS TTL only controls resolver caches, not + // authoritative record lifetime, so records do NOT auto-expire. + // After deletion we probe the address: if the device is still alive the kernel + // will emit a REACHABLE NewNeighbour event which re-registers it in DNS. + 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), + 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) = send_icmpv6_echo(addr, 0) { + debug!("reconcile: probe failed for {}: {}", addr, e); + } + } + Ok(IpAddr::V4(addr)) => { + if let Err(e) = send_icmpv4_echo(addr, 0) { + debug!("reconcile: probe failed for {}: {}", addr, e); + } + } + _ => {} + } + } + + // --- Registered orphans (in registered but not in DNS) --- + // hickory-dns may have restarted and lost its journal; re-push the record. + // Use the hostname stored in the entry — independent of the current lease table. + 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); + 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 new file mode 100644 index 00000000..919ff899 --- /dev/null +++ b/ipv6-neigh/src/types.rs @@ -0,0 +1,48 @@ +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) } +} + +#[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-app-mosdns/Makefile b/luci-app-mosdns/Makefile index 78ac6bc0..a006a80f 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns -PKG_VERSION:=1.7.2 -PKG_RELEASE:=8 +PKG_VERSION:=1.7.3 +PKG_RELEASE:=9 LUCI_TITLE:=LuCI Support for mosdns LUCI_PKGARCH:=all diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js index e4a01512..ebc0f4f6 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js @@ -19,6 +19,8 @@ const callGetUpdateLog = rpc.declare({ return view.extend({ handleUpdate() { + const statusMsg = E('p', { 'class': 'spinning' }, _('Please wait, this may take a few moments...')); + const logTextarea = E('textarea', { 'class': 'cbi-input-textarea', 'readonly': 'readonly', @@ -33,7 +35,7 @@ return view.extend({ }, _('Close')); ui.showModal(_('Updating Database...'), [ - E('p', { 'class': 'spinning' }, _('Please wait, this may take a few moments...')), + statusMsg, logTextarea, E('div', { 'class': 'right' }, [ closeButton ]) ]); @@ -45,14 +47,28 @@ return view.extend({ logTextarea.scrollTop = logTextarea.scrollHeight; if (res.log.match(/UPDATE_FINISHED/)) { - ui.hideModal(); - ui.addNotification(null, E('p', _('Update success')), 'info'); + statusMsg.textContent = _('Update success'); + statusMsg.classList.remove('spinning'); + statusMsg.style.color = '#19be6b'; + statusMsg.style.fontWeight = 'bold'; + closeButton.style.display = 'inline'; + return false; + } + + if (res.log.match(/UPDATE_EXITED/)) { + statusMsg.textContent = _('Update failed'); + statusMsg.classList.remove('spinning'); + statusMsg.style.color = '#ed4014'; + statusMsg.style.fontWeight = 'bold'; + closeButton.style.display = 'inline'; return false; } if (res.log.match(/Another update is already in progress/)) { - ui.hideModal(); - ui.addNotification(null, E('p', _('Another update is already in progress.')), 'warn'); + statusMsg.textContent = _('Another update is already in progress.'); + statusMsg.classList.remove('spinning'); + statusMsg.style.color = '#ff9900'; + closeButton.style.display = 'inline'; return false; } } @@ -70,12 +86,16 @@ return view.extend({ }); }, 1000); } else { - ui.hideModal(); - ui.addNotification(null, E('p', res.error || _('Failed to start update.')), 'error'); + statusMsg.textContent = res.error || _('Failed to start update.'); + statusMsg.classList.remove('spinning'); + statusMsg.style.color = '#ed4014'; + closeButton.style.display = 'inline'; } }).catch(e => { - ui.hideModal(); - ui.addNotification(null, E('p', _('Update failed: %s').format(e.message)), 'error'); + statusMsg.textContent = _('Update failed: %s').format(e.message); + statusMsg.classList.remove('spinning'); + statusMsg.style.color = '#ed4014'; + closeButton.style.display = 'inline'; }); }, diff --git a/luci-app-mosdns/po/templates/mosdns.pot b/luci-app-mosdns/po/templates/mosdns.pot index b459f005..a7ab71e6 100644 --- a/luci-app-mosdns/po/templates/mosdns.pot +++ b/luci-app-mosdns/po/templates/mosdns.pot @@ -74,7 +74,7 @@ msgstr "" msgid "Aliyun Public DNS (DNS over QUIC)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:55 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 msgid "Another update is already in progress." msgstr "" @@ -86,7 +86,7 @@ msgstr "" msgid "Auto Save Cache Interval" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:86 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 msgid "" "Automatically update GeoIP and GeoSite databases as well as ad filtering " "rules through scheduled tasks." @@ -133,11 +133,11 @@ msgstr "" msgid "Cache Dump" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:130 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 msgid "Check And Update" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:128 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 msgid "Check And Update GeoData." msgstr "" @@ -163,7 +163,7 @@ msgstr "" msgid "Clear logs" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:33 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 msgid "Close" msgstr "" @@ -246,7 +246,7 @@ msgstr "" msgid "DNS redirect" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:129 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:149 msgid "Database Update" msgstr "" @@ -276,7 +276,7 @@ msgstr "" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:91 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 msgid "Enable Auto Database Update" msgstr "" @@ -318,35 +318,35 @@ msgstr "" msgid "Error" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:115 msgid "Every Day" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:100 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:120 msgid "Every Friday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:96 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 msgid "Every Monday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:101 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 msgid "Every Saturday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:102 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 msgid "Every Sunday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:99 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:119 msgid "Every Thursday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:97 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 msgid "Every Tuesday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:98 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:118 msgid "Every Wednesday" msgstr "" @@ -359,7 +359,7 @@ msgstr "" msgid "Failed to clean logs." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:74 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 msgid "Failed to start update." msgstr "" @@ -399,11 +399,11 @@ msgstr "" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 msgid "Full" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:134 msgid "Full: includes all Countries and Private IP addresses." msgstr "" @@ -415,7 +415,7 @@ msgstr "" msgid "GeoIP Tags" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 msgid "GeoIP Type" msgstr "" @@ -427,7 +427,7 @@ msgstr "" msgid "Geodata Update" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:141 msgid "GitHub Proxy" msgstr "" @@ -503,11 +503,11 @@ msgstr "" msgid "Listen Port" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:137 msgid "Little" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:112 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 msgid "Little: only include Mainland China and Private IP addresses." msgstr "" @@ -585,7 +585,7 @@ msgid "" "Media DNS requests" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:36 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:22 msgid "Please wait, this may take a few moments..." msgstr "" @@ -641,7 +641,7 @@ msgstr "" msgid "Save the cache locally and reload the cache dump on the next startup" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:26 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:28 msgid "Starting update..." msgstr "" @@ -721,32 +721,36 @@ msgstr "" msgid "Unable to save contents: %s" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:94 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 msgid "Update Cycle" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:85 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 msgid "Update GeoIP & GeoSite databases" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 msgid "Update Time" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 msgid "" "Update data files with GitHub Proxy, leave blank to disable proxy downloads." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:78 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:59 +msgid "Update failed" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 msgid "Update failed: %s" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:49 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:50 msgid "Update success" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:37 msgid "Updating Database..." msgstr "" @@ -781,6 +785,6 @@ msgstr "" msgid "Xinfeng Public DNS (114.114.115.115)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:123 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "https://gh-proxy.com" msgstr "" diff --git a/luci-app-mosdns/po/zh_Hans/mosdns.po b/luci-app-mosdns/po/zh_Hans/mosdns.po index 74562a1b..631b83cd 100644 --- a/luci-app-mosdns/po/zh_Hans/mosdns.po +++ b/luci-app-mosdns/po/zh_Hans/mosdns.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-20 09:23+0800\n" -"PO-Revision-Date: 2026-05-01 09:13+0800\n" +"PO-Revision-Date: 2026-05-15 14:09+0800\n" "Last-Translator: sbwml \n" "Language-Team: Chinese\n" "Language: zh_Hans\n" @@ -89,7 +89,7 @@ msgstr "阿里云公共 DNS(DNS over HTTPS)" msgid "Aliyun Public DNS (DNS over QUIC)" msgstr "阿里云公共 DNS(DNS over QUIC)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:55 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 msgid "Another update is already in progress." msgstr "另一个更新正在进行中。" @@ -101,7 +101,7 @@ msgstr "Apple 域名解析优化" msgid "Auto Save Cache Interval" msgstr "自动保存缓存间隔(秒)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:86 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 msgid "" "Automatically update GeoIP and GeoSite databases as well as ad filtering " "rules through scheduled tasks." @@ -150,11 +150,11 @@ msgstr "Bootstrap DNS 服务器用于解析您指定为上游的 DoH / DoT 解 msgid "Cache Dump" msgstr "自动保存缓存" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:130 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 msgid "Check And Update" msgstr "检查并更新" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:128 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 msgid "Check And Update GeoData." msgstr "检查并更新 GeoData 数据库。" @@ -180,7 +180,7 @@ msgstr "思科公共 DNS(208.67.222.222)" msgid "Clear logs" msgstr "清空日志" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:33 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 msgid "Close" msgstr "关闭" @@ -264,7 +264,7 @@ msgstr "DNS 查询请求并发数,允许同时发起请求的上游 DNS 服务 msgid "DNS redirect" msgstr "DNS 重定向" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:129 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:149 msgid "Database Update" msgstr "数据库更新" @@ -295,7 +295,7 @@ msgstr "" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgstr "DoH/TCP/DoT 连接复用空闲保持时间(默认 30 秒)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:91 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 msgid "Enable Auto Database Update" msgstr "启用自动更新" @@ -337,35 +337,35 @@ msgstr "填写需要导出的 GeoSite.dat 类别条目,允许添加多个标 msgid "Error" msgstr "错误" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:115 msgid "Every Day" msgstr "每天" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:100 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:120 msgid "Every Friday" msgstr "每周五" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:96 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 msgid "Every Monday" msgstr "每周一" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:101 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 msgid "Every Saturday" msgstr "每周六" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:102 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 msgid "Every Sunday" msgstr "每周日" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:99 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:119 msgid "Every Thursday" msgstr "每周四" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:97 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 msgid "Every Tuesday" msgstr "每周二" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:98 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:118 msgid "Every Wednesday" msgstr "每周三" @@ -378,7 +378,7 @@ msgstr "导出目录:/var/mosdns" msgid "Failed to clean logs." msgstr "清理日志失败:%s" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:74 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 msgid "Failed to start update." msgstr "启动更新失败" @@ -418,11 +418,11 @@ msgstr "强制将所有本地 DNS 查询重定向到 MosDNS,即 DNS 劫持。" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgstr "将 Dnsmasq 域名解析请求转发到 MosDNS 服务器" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 msgid "Full" msgstr "全量" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:134 msgid "Full: includes all Countries and Private IP addresses." msgstr "全量:包含所有国家和私有 IP 地址。" @@ -434,7 +434,7 @@ msgstr "GeoData 导出" msgid "GeoIP Tags" msgstr "GeoIP 标签" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 msgid "GeoIP Type" msgstr "GeoIP 类型" @@ -446,7 +446,7 @@ msgstr "GeoSite 标签" msgid "Geodata Update" msgstr "更新数据库" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:141 msgid "GitHub Proxy" msgstr "GitHub 代理" @@ -463,7 +463,7 @@ msgstr "谷歌公共 DNS(8.8.8.8)" #: luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json:3 msgid "Grant UCI access for luci-app-mosdns" -msgstr "" +msgstr "授予 luci-app-mosdns 的 UCI 访问权限" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:20 msgid "Grey Lists" @@ -522,11 +522,11 @@ msgstr "监听地址" msgid "Listen Port" msgstr "监听端口" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:137 msgid "Little" msgstr "轻量" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:112 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 msgid "Little: only include Mainland China and Private IP addresses." msgstr "轻量:仅包含中国大陆和私有 IP 地址。" @@ -609,7 +609,7 @@ msgstr "" "请提供您在访问国外网站时使用的 IP 地址,这个 IP 子网(0/24)将用作 远程 / 流" "媒体 DNS 请求的 ECS 地址" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:36 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:22 msgid "Please wait, this may take a few moments..." msgstr "请稍候,这可能需要一点时间..." @@ -667,7 +667,7 @@ msgstr "规则列表" msgid "Save the cache locally and reload the cache dump on the next startup" msgstr "保存缓存到本地文件,以供下次启动时重新载入使用" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:26 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:28 msgid "Starting update..." msgstr "" @@ -751,32 +751,36 @@ msgstr "火山引擎公共 DNS(180.184.2.2)" msgid "Unable to save contents: %s" msgstr "无法保存内容:%s" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:94 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 msgid "Update Cycle" msgstr "更新周期" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:85 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 msgid "Update GeoIP & GeoSite databases" msgstr "更新广告规则、GeoIP & GeoSite 数据库" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 msgid "Update Time" msgstr "更新时间" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 msgid "" "Update data files with GitHub Proxy, leave blank to disable proxy downloads." msgstr "通过 GitHub 代理更新数据文件,留空则禁用代理下载。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:78 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:59 +msgid "Update failed" +msgstr "更新失败" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 msgid "Update failed: %s" msgstr "更新失败: %s" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:49 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:50 msgid "Update success" msgstr "更新成功" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:37 msgid "Updating Database..." msgstr "更新数据库..." @@ -815,6 +819,6 @@ msgstr "信风公共 DNS(114.114.114.114)" msgid "Xinfeng Public DNS (114.114.115.115)" msgstr "信风公共 DNS(114.114.115.115)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:123 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "https://gh-proxy.com" msgstr "" diff --git a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc index 487a369c..345d41b3 100755 --- a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc +++ b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc @@ -158,7 +158,7 @@ function update_adlist() { print("\x1b[1;31mRules download failed.\n"); exec_sys(`rm -rf "${ad_tmpdir}"`); unlink(lock_file); - exit(1); + die("Rules download failed."); } else { if (has_update) { mkdir('/etc/mosdns/rule/adlist', 0755); @@ -183,7 +183,7 @@ function update_geodat() { let v2dat_dir = '/usr/share/v2ray'; let tmp_res = exec_sys('mktemp -d'); - if (tmp_res.code !== 0) exit(1); + if (tmp_res.code !== 0) die("Failed to create temp directory for geodata."); let tmpdir = tmp_res.stdout; exec_sys(`mkdir -p "${v2dat_dir}"`); @@ -194,7 +194,7 @@ function update_geodat() { print(`Downloading ${geoip_url}.sha256sum\n`); stdout.flush(); if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geoip.dat.sha256sum" "${geoip_url}.sha256sum"`).code !== 0) { - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geoip.dat.sha256sum"); } let geoip_sum_remote = split(exec_sys(`cat "${tmpdir}/geoip.dat.sha256sum"`).stdout, /[ \t\n]+/)[0]; @@ -210,13 +210,13 @@ function update_geodat() { print(`Downloading ${geoip_url}\n`); stdout.flush(); if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geoip.dat" "${geoip_url}"`).code !== 0) { - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geoip.dat"); } let sum_downloaded = split(exec_sys(`sha256sum "${tmpdir}/geoip.dat"`).stdout, /[ \t\n]+/)[0]; if (sum_downloaded !== geoip_sum_remote) { print("\x1b[1;31mgeoip.dat checksum error\n"); - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("geoip.dat checksum error"); } geoip_updated = true; } @@ -227,7 +227,7 @@ function update_geodat() { print(`Downloading ${geosite_url}.sha256sum\n`); stdout.flush(); if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geosite.dat.sha256sum" "${geosite_url}.sha256sum"`).code !== 0) { - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geosite.dat.sha256sum"); } let geosite_sum_remote = split(exec_sys(`cat "${tmpdir}/geosite.dat.sha256sum"`).stdout, /[ \t\n]+/)[0]; @@ -243,13 +243,13 @@ function update_geodat() { print(`Downloading ${geosite_url}\n`); stdout.flush(); if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geosite.dat" "${geosite_url}"`).code !== 0) { - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geosite.dat"); } let sum_downloaded = split(exec_sys(`sha256sum "${tmpdir}/geosite.dat"`).stdout, /[ \t\n]+/)[0]; if (sum_downloaded !== geosite_sum_remote) { print("\x1b[1;31mgeosite.dat checksum error\n"); - exec_sys(`rm -rf "${tmpdir}"`); exit(1); + exec_sys(`rm -rf "${tmpdir}"`); die("geosite.dat checksum error"); } geosite_updated = true; } @@ -332,9 +332,11 @@ switch (action) { print("UPDATE_FINISHED\n"); stdout.flush(); } catch (e) { - print("Update failed: " + e + "\n"); - print("UPDATE_FINISHED\n"); + print("\x1b[1;31mUpdate failed: " + e + "\n\x1b[0m"); + print("UPDATE_EXITED\n"); stdout.flush(); + unlink('/var/lock/mosdns_update.lock'); + exit(1); } unlink('/var/lock/mosdns_update.lock'); break; diff --git a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns index 9804ad58..83afc4c5 100644 --- a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns +++ b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns @@ -128,7 +128,7 @@ const methods = { return { success: false, error: 'Another update is already in progress.' }; } exec_sys('echo "" > /var/log/mosdns_update.log'); - exec_sys('{ /usr/share/mosdns/mosdns.uc update; rm -f /var/lock/mosdns_update.lock; echo "UPDATE_FINISHED"; } > /var/log/mosdns_update.log 2>&1 &'); + exec_sys('/usr/share/mosdns/mosdns.uc update > /var/log/mosdns_update.log 2>&1 &'); return { success: true }; } catch (e) { return { success: false, error: String(e) }; diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 0d9a1766..f9a85cb1 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.5.11 -PKG_RELEASE:=123 +PKG_RELEASE:=124 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index c4fe8a46..907c49a3 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -1205,7 +1205,7 @@ function to_move(app_name,file) } end - local flag = sys.call('pgrep -af "passwall/.*'.. app_name ..'" >/dev/null') + local flag = sys.call('busybox pgrep -af "passwall/.*'.. app_name ..'" >/dev/null') if flag == 0 then sys.call("/etc/init.d/passwall stop") end diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index a6cf3c30..c2c53764 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1054,7 +1054,7 @@ socks_node_switch() { [ -s "$pf" ] && kill -9 "$(head -n1 "$pf")" >/dev/null 2>&1 done - pgrep -af "$TMP_BIN_PATH" | awk -v P1="${flag}" 'BEGIN{IGNORECASE=1}$0~P1 && !/acl\/|acl_/{print $1}' | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -af "$TMP_BIN_PATH" | awk -v P1="${flag}" 'BEGIN{IGNORECASE=1}$0~P1 && !/acl\/|acl_/{print $1}' | xargs kill -9 >/dev/null 2>&1 for prefix in "" "HTTP_" "HTTP2"; do rm -rf "$TMP_PATH/${prefix}SOCKS_${flag}"* done @@ -1092,7 +1092,7 @@ clean_crontab() { sed -i "/$(echo "lua ${APP_PATH}/rule_update.lua log" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 sed -i "/$(echo "lua ${APP_PATH}/subscribe.lua start" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 - pgrep -af "${CONFIG}/" | awk '/tasks\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -af "${CONFIG}/" | awk '/tasks\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 rm -f ${LOCK_PATH}/${CONFIG}_tasks.lock } @@ -1941,8 +1941,8 @@ stop() { kill -9 "$pid" >/dev/null 2>&1 fi done - pgrep -f "sleep.*(6s|9s|58s)" | xargs kill -9 >/dev/null 2>&1 - pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua|tasks\.sh|server_app\.lua|ujail/{print $1}' | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -f "sleep.*(6s|9s|58s)" | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua|tasks\.sh|server_app\.lua|ujail/{print $1}' | xargs kill -9 >/dev/null 2>&1 unset V2RAY_LOCATION_ASSET unset XRAY_LOCATION_ASSET unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh b/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh index 3f485f9d..3b5700f3 100755 --- a/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh +++ b/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh @@ -8,7 +8,7 @@ listen_port=$2 server_address=$3 server_port=$4 -pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null && { +busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null && { # 特定任务执行中不检测 exit 0 } diff --git a/luci-app-passwall/root/usr/share/passwall/monitor.sh b/luci-app-passwall/root/usr/share/passwall/monitor.sh index 839b5aa9..5314b7d4 100755 --- a/luci-app-passwall/root/usr/share/passwall/monitor.sh +++ b/luci-app-passwall/root/usr/share/passwall/monitor.sh @@ -38,7 +38,7 @@ while [ 1 -eq 1 ]; do # 检查是否超过最大重启次数 [ "$restart_count" -ge "$MAX_RESTART_COUNT" ] && continue - if ! pgrep -f "$cmd_check" >/dev/null; then + if ! busybox pgrep -f "$cmd_check" >/dev/null; then restart_count=$((restart_count + 1)) echo "$restart_count" > "$stats_file" #echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log diff --git a/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh b/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh index dac69da2..38d9b773 100755 --- a/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh +++ b/luci-app-passwall/root/usr/share/passwall/socks_auto_switch.sh @@ -6,7 +6,7 @@ APP_FILE=${APP_PATH}/app.sh flag=0 check_process() { - while pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null; do + while busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null; do sleep 6s done } @@ -63,7 +63,7 @@ test_node() { # 结束 SS 插件进程 local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid" [ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1 - pgrep -af "test_node_${node_id}" | awk '! /socks_auto_switch\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -af "test_node_${node_id}" | awk '! /socks_auto_switch\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 rm -rf /tmp/etc/${CONFIG}/test_node_${node_id}*.* if [ "${_proxy_status}" -eq 200 ]; then return 0 diff --git a/luci-app-passwall/root/usr/share/passwall/test.sh b/luci-app-passwall/root/usr/share/passwall/test.sh index c9b9ea43..72ca0a60 100755 --- a/luci-app-passwall/root/usr/share/passwall/test.sh +++ b/luci-app-passwall/root/usr/share/passwall/test.sh @@ -66,7 +66,7 @@ url_test_node() { # 结束 SS 插件进程 local pid_file="/tmp/etc/${CONFIG}/url_test_${node_id}_plugin.pid" [ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1 - pgrep -af "url_test_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + busybox pgrep -af "url_test_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 rm -rf /tmp/etc/${CONFIG}/*url_test_${node_id}*.* } echo $result diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile index 34ca5e2d..14938f91 100644 --- a/luci-mod-status/Makefile +++ b/luci-mod-status/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Status Pages LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_BUILD_DEPENDS:=iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/15_ports.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/15_ports.js index 1b8f5d1f..16499d67 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/15_ports.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/15_ports.js @@ -230,6 +230,40 @@ function addBoardNetworkPorts(knownPorts, seenPorts, role, entry, mapping, board addResolvedPort(knownPorts, seenPorts, role, value, mapping, board, swstate); } +function forEachBoardNetworkEntry(board, cb) +{ + if (!L.isObject(board) || !L.isObject(board.network)) + return; + + for (const role in board.network) + if (L.isObject(board.network[role])) + cb(role, board.network[role]); +} + +function isEnumeratedEthernetPort(dev) +{ + if (!L.isObject(dev) || typeof(dev.getName) !== 'function' || + typeof(dev.getType) !== 'function' || typeof(dev._devstate) !== 'function') + return false; + + const name = dev.getName(); + const type = dev.getType(); + + return isString(name) && /^eth\d+$/.test(name) && + dev._devstate('type') == 1 && + (type == 'ethernet' || type == 'switch'); +} + +function addSystemEthernetPorts(knownPorts, seenPorts, devices, mapping, board, swstate) +{ + if (!Array.isArray(devices)) + return; + + for (const dev of devices) + if (isEnumeratedEthernetPort(dev)) + addResolvedPort(knownPorts, seenPorts, 'unknown', dev.getName(), mapping, board, swstate); +} + function resolveVLANChain(ifname, bridges, mapping) { while (!mapping[ifname]) { @@ -630,10 +664,12 @@ return baseclass.extend({ L.resolveDefault(fs.read('/etc/board.json'), '{}'), firewall.getZones(), network.getNetworks(), + network.getDevices(), uci.load('network') ]).then((data) => { const builtinPorts = data[0] || []; const board = JSON.parse(data[1] || '{}'); + const devices = data[4] || []; const allPorts = new Set(); const swstate = {}; const tasks = []; @@ -643,16 +679,18 @@ return baseclass.extend({ allPorts.add(port.device); }); - if (allPorts.size === 0 && board.network) { - ['lan', 'wan'].forEach((role) => { - if (board.network[role]) { - if (Array.isArray(board.network[role].ports)) - board.network[role].ports.forEach((p) => allPorts.add(p)); - else if (board.network[role].device) - allPorts.add(board.network[role].device); - } - }); - } + forEachBoardNetworkEntry(board, function(role, entry) { + parseBoardPortList(entry.ports).forEach((p) => allPorts.add(p)); + parseBoardPortList(entry.ifname).forEach((p) => allPorts.add(p)); + + if (isString(entry.device)) + allPorts.add(entry.device); + }); + + devices.forEach((dev) => { + if (isEnumeratedEthernetPort(dev)) + allPorts.add(dev.getName()); + }); if (L.isObject(board) && L.isObject(board.switch)) { for (const switchName in board.switch) { @@ -689,9 +727,10 @@ return baseclass.extend({ render(data) { const board = JSON.parse(data[1]), - swstate = data[5] || {}, + devices = data[4] || [], + swstate = data[6] || {}, port_map = buildInterfaceMapping(data[2], data[3], board), - pseMap = data[6] || {}; + pseMap = data[7] || {}; let known_ports = []; const seenPorts = {}; const vlanMap = {}; @@ -705,14 +744,11 @@ return baseclass.extend({ }, []); } - if (L.isObject(board) && L.isObject(board.network)) { - for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) { - if (!L.isObject(board.network[k])) - continue; + forEachBoardNetworkEntry(board, function(role, entry) { + addBoardNetworkPorts(known_ports, seenPorts, role, entry, vlanMap, board, swstate); + }); - addBoardNetworkPorts(known_ports, seenPorts, k, board.network[k], vlanMap, board, swstate); - } - } + addSystemEthernetPorts(known_ports, seenPorts, devices, vlanMap, board, swstate); if (!known_ports.length) return null; diff --git a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json index cdf1a608..a69418f9 100644 --- a/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json +++ b/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status-index.json @@ -52,7 +52,8 @@ "description": "Grant access to port status display", "read": { "ubus": { - "luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ] + "luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ], + "luci-rpc": [ "getNetworkDevices" ] } } }, diff --git a/mihomo-alpha/Makefile b/mihomo-alpha/Makefile index 795baff8..868d7348 100644 --- a/mihomo-alpha/Makefile +++ b/mihomo-alpha/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mihomo-alpha PKG_VERSION:=2026.05.08 -PKG_RELEASE:=6 +PKG_RELEASE:=7 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) @@ -22,6 +22,7 @@ PKG_BUILD_VERSION:=alpha-35d5d4e4 PKG_BUILD_TIME:=$(shell date -u -Iseconds) GO_PKG:=github.com/metacubex/mihomo +GO_PKG_BUILD_PKG:=github.com/metacubex/mihomo GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME) GO_PKG_TAGS:=with_gvisor GO_PKG_INSTALL_BIN_PATH:=/usr/libexec diff --git a/mihomo-meta/Makefile b/mihomo-meta/Makefile index 64d3e94a..331fb286 100644 --- a/mihomo-meta/Makefile +++ b/mihomo-meta/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mihomo-meta PKG_VERSION:=1.19.24 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) @@ -22,6 +22,7 @@ PKG_BUILD_VERSION:=v1.19.24 PKG_BUILD_TIME:=$(shell date -u -Iseconds) GO_PKG:=github.com/metacubex/mihomo +GO_PKG_BUILD_PKG:=github.com/metacubex/mihomo GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME) GO_PKG_TAGS:=with_gvisor GO_PKG_INSTALL_BIN_PATH:=/usr/libexec diff --git a/qbittorrent/Makefile b/qbittorrent/Makefile new file mode 100644 index 00000000..d71d9733 --- /dev/null +++ b/qbittorrent/Makefile @@ -0,0 +1,62 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=qBittorrent +PKG_VERSION:=5.2.0 +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/qbittorrent/qBittorrent/tar.gz/release-$(PKG_VERSION)? +PKG_HASH:=skip +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-release-$(PKG_VERSION) + +PKG_LICENSE:=GPL-2.0-or-later +PKG_LICENSE_FILES:=COPYING +PKG_CPE_ID:=cpe:/a:qbittorrent:qbittorrent + +PKG_BUILD_DEPENDS:=qt6tools/host +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/qbittorrent + SUBMENU:=BitTorrent + SECTION:=net + CATEGORY:=Network + TITLE:=A BitTorrent client in Qt + URL:=https://www.qbittorrent.org/ + DEPENDS:=+libtorrent-rasterbar +libQt6Core +libQt6Network +libQt6Sql \ + +libQt6Xml +qt6-plugin-libqopensslbackend +qt6-plugin-libqsqlite +endef + +define Package/qbittorrent/description + qBittorrent is a bittorrent client programmed in C++ / Qt that uses + libtorrent (sometimes called libtorrent-rasterbar) by Arvid Norberg. + It aims to be a good alternative to all other bittorrent clients out + there. qBittorrent is fast, stable and provides unicode support as + well as many features. +endef + +define Package/qbittorrent/conffiles +/etc/config/qbittorrent +/etc/qBittorrent/ +endef + +CMAKE_OPTIONS+= \ + -DGUI=OFF \ + -DQT6=ON \ + -DSTACKTRACE=OFF \ + -DWEBUI=ON \ + -DQT_ADDITIONAL_PACKAGES_PREFIX_PATH=$(STAGING_DIR_HOSTPKG) + +define Package/qbittorrent/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/qbittorrent-nox $(1)/usr/bin + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) $(CURDIR)/files/qbittorrent.config $(1)/etc/config/qbittorrent + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) $(CURDIR)/files/qbittorrent.init $(1)/etc/init.d/qbittorrent +endef + +$(eval $(call BuildPackage,qbittorrent)) diff --git a/qbittorrent/files/qbittorrent.config b/qbittorrent/files/qbittorrent.config new file mode 100644 index 00000000..cfc10293 --- /dev/null +++ b/qbittorrent/files/qbittorrent.config @@ -0,0 +1,6 @@ + +config qbittorrent 'config' + option enabled '0' + option download_dir '/mnt/download' + option http_port '8080' + diff --git a/qbittorrent/files/qbittorrent.init b/qbittorrent/files/qbittorrent.init new file mode 100644 index 00000000..c8883603 --- /dev/null +++ b/qbittorrent/files/qbittorrent.init @@ -0,0 +1,79 @@ +#!/bin/sh /etc/rc.common + +USE_PROCD=1 +START=99 + +CONF="qbittorrent" +PROG="/usr/bin/qbittorrent-nox" + +QBT_CONF="/etc/qBittorrent/config/qBittorrent.conf" +QBT_LOG="/var/log/$CONF" + +start_service() { + config_load "$CONF" + + local enabled + config_get_bool enabled "config" "enabled" "0" + [ "$enabled" -eq "1" ] || return 1 + + local download_dir http_port + config_get download_dir "config" "download_dir" "/mnt/sda1" + config_get http_port "config" "http_port" "8080" + + [ -d "$download_dir" ] || mkdir -p "$download_dir" + + if [ -f "$QBT_CONF" ]; then + sed -e "s,WebUI\\\Port=[0-9]*,WebUI\\\Port=$http_port,g" \ + -e "s,Session\\\DefaultSavePath=.*,Session\\\DefaultSavePath=$download_dir,g" \ + -i "$QBT_CONF" + else + mkdir -p "${QBT_CONF%/*}" + cat > "$QBT_CONF" <<-EOF + [Application] + FileLogger\Path=$QBT_LOG + + [AutoRun] + enabled=false + program= + + [BitTorrent] + Session\AddTrackersFromURLEnabled=true + Session\AdditionalTrackersURL=https://trackerslist.com/all.txt + Session\DefaultSavePath=$download_dir + + [LegalNotice] + Accepted=true + + [Network] + Cookies=@Invalid() + + [Preferences] + WebUI\CSRFProtection=false + WebUI\Password_PBKDF2="@ByteArray(zH92gnE9xijRN5IjzIPU+A==:+JxpKBWsSyuzpm/9LrhO2uLQSGsBS5giqF0AYRU8COcMXn5AIaSsL2S9hIuB20wkhsMfEoN+77Q9BmAd3ysYxw==)" + WebUI\Port=$http_port + EOF + fi + + procd_open_instance + procd_set_param command "$PROG" + procd_append_param command "--profile=/etc" + + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param respawn + procd_set_param stderr 1 + + procd_close_instance +} + +stop_service() { + rm -rf "$QBT_LOG" +} + +service_triggers() { + procd_add_reload_trigger "$CONF" +} + +reload_service() { + restart +} diff --git a/qbittorrent/patches/010-fix-build-with-recent-fortify.patch b/qbittorrent/patches/010-fix-build-with-recent-fortify.patch new file mode 100644 index 00000000..4a645af9 --- /dev/null +++ b/qbittorrent/patches/010-fix-build-with-recent-fortify.patch @@ -0,0 +1,10 @@ +--- a/cmake/Modules/CommonConfig.cmake ++++ b/cmake/Modules/CommonConfig.cmake +@@ -60,7 +60,6 @@ if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU + -Wold-style-cast + -Wnon-virtual-dtor + -pedantic +- -pedantic-errors + ) + + # Clang 11 still doesn't support -Wstrict-null-sentinel diff --git a/qmbimat/Makefile b/qmbimat/Makefile index fad1ffe7..a58c9cde 100644 --- a/qmbimat/Makefile +++ b/qmbimat/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=qmbimat PKG_VERSION:=0.0.1 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_MAINTAINER:=Konstantine Shevlakov PKG_LICENSE:=LICENSE diff --git a/qmbimat/patches/0002-qmbimat_more_fixes.patch b/qmbimat/patches/0002-qmbimat_more_fixes.patch new file mode 100644 index 00000000..6998a06a --- /dev/null +++ b/qmbimat/patches/0002-qmbimat_more_fixes.patch @@ -0,0 +1,327 @@ +diff -ruN QMbimAT-main/src/main.c QMbimAT-main-fixed/src/main.c +--- QMbimAT-main/src/main.c 2023-09-05 19:22:17.000000000 +0000 ++++ QMbimAT-main-fixed/src/main.c 2026-05-15 07:14:30.636615920 +0000 +@@ -54,7 +54,7 @@ + return 0; + } + +- while (-1 != (opt = getopt_long(argc, argv, "a:d:vh", longopts, NULL))) ++ while (-1 != (opt = getopt_long(argc, argv, "a:d:vhD", longopts, NULL))) + { + switch (opt) + { +@@ -143,13 +143,10 @@ + + int main(int argc, char *argv[]) + { +- if (argc >= 5) ++ if (argc <= 1) + { +- return debug_tool(argc, argv ); +- } +- else +- { +- debug_tool_usage(argv[0]); ++ debug_tool_usage(); + return -1; + } ++ return debug_tool(argc, argv); + } +diff -ruN QMbimAT-main/src/mbim_ctx.c QMbimAT-main-fixed/src/mbim_ctx.c +--- QMbimAT-main/src/mbim_ctx.c 2023-09-05 19:22:17.000000000 +0000 ++++ QMbimAT-main-fixed/src/mbim_ctx.c 2026-05-15 06:58:01.288358305 +0000 +@@ -129,14 +129,37 @@ + } + else if (mbim_pRequest && le32toh(mbim_pRequest->TransactionId) == le32toh(pResponse->TransactionId)) + { +- mbim_pResponse = mbim_alloc(le32toh(pResponse->MessageLength) + 1); +- if (mbim_pResponse) +- memcpy(mbim_pResponse, pResponse, le32toh(pResponse->MessageLength)); +- pthread_cond_signal(&mbim_command_cond); ++ uint32_t resp_type = le32toh(pResponse->MessageType); ++ uint32_t req_type = le32toh(mbim_pRequest->MessageType); ++ ++ /* Accept matching response types only: ++ * MBIM_OPEN_MSG -> MBIM_OPEN_DONE ++ * MBIM_CLOSE_MSG -> MBIM_CLOSE_DONE ++ * MBIM_COMMAND_MSG -> MBIM_COMMAND_DONE ++ * Also accept MBIM_FUNCTION_ERROR_MSG for any request. ++ */ ++ int is_expected = (resp_type == MBIM_FUNCTION_ERROR_MSG) || ++ (req_type == MBIM_OPEN_MSG && resp_type == MBIM_OPEN_DONE) || ++ (req_type == MBIM_CLOSE_MSG && resp_type == MBIM_CLOSE_DONE) || ++ (req_type == MBIM_COMMAND_MSG && resp_type == MBIM_COMMAND_DONE); ++ ++ if (is_expected) ++ { ++ mbim_pResponse = mbim_alloc(le32toh(pResponse->MessageLength) + 1); ++ if (mbim_pResponse) ++ memcpy(mbim_pResponse, pResponse, le32toh(pResponse->MessageLength)); ++ pthread_cond_signal(&mbim_command_cond); ++ } ++ else ++ { ++ mbim_debug("info: ignoring MessageType=0x%x for TransactionId=%u (waiting for response to req type=0x%x)", ++ resp_type, le32toh(pResponse->TransactionId), req_type); ++ } + } + else if (le32toh(pResponse->MessageType) == MBIM_INDICATE_STATUS_MSG) + { + MBIM_INDICATE_STATUS_MSG_T *pIndMsg = (MBIM_INDICATE_STATUS_MSG_T *)pResponse; ++ (void)pIndMsg; + } + + pthread_mutex_unlock(&mbim_command_mutex); +@@ -207,7 +230,7 @@ + mbim_debug("%s is created", __func__); + (void)param; + +- while (mbim_fd > 0) ++ while (mbim_fd >= 0) + { + struct pollfd pollfds[] = {{mbim_fd, POLLIN, 0}, {control_pipe[0], POLLIN, 0}}; + int ne, ret, nevents = 2; +@@ -247,21 +270,22 @@ + mbim_debug("%s read=%d errno: %d (%s)", __func__, (int)nreads, errno, strerror(errno)); + break; + } +- else if (nreads < pResponse->MessageLength) ++ else if (nreads < (ssize_t)le32toh(pResponse->MessageLength)) + { +- mbim_debug("error: %s read=%d MessageLength=%u", __func__, (int)nreads, pResponse->MessageLength); ++ mbim_debug("error: %s read=%d MessageLength=%u", __func__, (int)nreads, le32toh(pResponse->MessageLength)); + break; + } +- else if (nreads >= pResponse->MessageLength) ++ else if (nreads >= (ssize_t)le32toh(pResponse->MessageLength)) + { + while (nreads > 0) + { +- mbim_debug("%s read=%d MessageLength=%u", __func__, (int)nreads, pResponse->MessageLength); ++ uint32_t msg_len = le32toh(pResponse->MessageLength); ++ mbim_debug("%s read=%d MessageLength=%u", __func__, (int)nreads, msg_len); + + // coverity[tainted_data:FALSE] +- mbim_recv_command(pResponse, pResponse->MessageLength); +- nreads -= pResponse->MessageLength; +- pResponse = (MBIM_MESSAGE_HEADER *)((char *)pResponse + pResponse->MessageLength); ++ mbim_recv_command(pResponse, msg_len); ++ nreads -= msg_len; ++ pResponse = (MBIM_MESSAGE_HEADER *)((char *)pResponse + msg_len); + } + } + } +@@ -350,7 +374,7 @@ + if (control_pipe[0] == -1) + return; + +- if (!use_mbim_proxy && mbim_fd) ++ if (!use_mbim_proxy && mbim_fd != -1) + { + mbim_CLOSE(); + } +@@ -387,7 +411,7 @@ + if (mbim_fd != -1) + return 0; + +- fp = popen("ps -e | grep mbim-proxy", "r"); ++ fp = popen("cat /proc/[0-9]*/comm 2>/dev/null | grep -m1 mbim-proxy", "r"); + if (fp != NULL) + { + // coverity[check_return] +diff -ruN QMbimAT-main/src/mbim_protocol.c QMbimAT-main-fixed/src/mbim_protocol.c +--- QMbimAT-main/src/mbim_protocol.c 2023-09-05 19:22:17.000000000 +0000 ++++ QMbimAT-main-fixed/src/mbim_protocol.c 2026-05-15 07:25:01.090234223 +0000 +@@ -161,6 +161,8 @@ + return &uuid; + } + ++static const UUID_T *str2uuid_pub(const char *str) { return str2uuid(str); } ++ + static uint32_t TransactionId(void) + { + static uint32_t tid = 0; +@@ -295,7 +297,7 @@ + return NULL; + + pOpen->MessageHeader.MessageType = htole32(MBIM_OPEN_MSG); +- pOpen->MessageHeader.MessageLength = htole32(sizeof(MBIM_COMMAND_MSG_T)); ++ pOpen->MessageHeader.MessageLength = htole32(sizeof(MBIM_OPEN_MSG_T)); + pOpen->MessageHeader.TransactionId = htole32(TransactionId()); + pOpen->MaxControlTransfer = htole32(4096); + return (MBIM_MESSAGE_HEADER*)pOpen; +@@ -309,7 +311,7 @@ + return NULL; + + pOpen->MessageHeader.MessageType = htole32(MBIM_CLOSE_MSG); +- pOpen->MessageHeader.MessageLength = htole32(sizeof(MBIM_COMMAND_MSG_T)); ++ pOpen->MessageHeader.MessageLength = htole32(sizeof(MBIM_CLOSE_MSG_T)); + pOpen->MessageHeader.TransactionId = htole32(TransactionId()); + + return (MBIM_MESSAGE_HEADER*)pOpen; +@@ -361,9 +363,10 @@ + { + cfg = (MBIM_LIBQMI_PROXY_CONFIG_T *)((MBIM_COMMAND_MSG_T *)pRequest)->InformationBuffer; + +- cfg->DevicePathOffset = sizeof(*cfg); +- cfg->DevicePathSize = char2wchar((const uint8_t *)dev, strlen(dev), cfg->DataBuffer, strlen(dev) * 2); +- cfg->Timeout = 15; ++ uint32_t wchar_len = (uint32_t)char2wchar((const uint8_t *)dev, strlen(dev), cfg->DataBuffer, strlen(dev) * 2); ++ cfg->DevicePathOffset = htole32(sizeof(*cfg)); ++ cfg->DevicePathSize = htole32(wchar_len); ++ cfg->Timeout = htole32(15); + } + + err = mbim_send_command(pRequest, &pCmdDone); +@@ -555,6 +558,101 @@ + return err; + } + ++ ++/* AT-over-MBIM service candidates, tried in order */ ++typedef struct { ++ const char *uuid; ++ uint32_t cid; ++ const char *description; ++} AT_SERVICE_CANDIDATE; ++ ++static const AT_SERVICE_CANDIDATE at_service_candidates[] = { ++ /* Most Quectel LTE/5G modules: EC2x, EP06, EM06, EG06, EG12, EG18, ++ EM12, EM160, RG500, RM500, RM502, RM520, etc. */ ++ { uuid_ext_qmux, 1, "EXT_QMUX/CID=1 (EC2x/EP06/EM06/EG-series/RM5xx)" }, ++ /* Older fallback: EM060 and similar SDX55-based modules */ ++ { uuid_qdu, 8, "QDU/CID=8 (EM060)" }, ++ /* Sentinel */ ++ { NULL, 0, NULL } ++}; ++ ++static const char *s_at_uuid = NULL; ++static uint32_t s_at_cid = 0; ++ ++/* Query Device Services and pick the first UUID/CID pair the modem supports */ ++static int mbim_detect_at_service(void) ++{ ++ MBIM_MESSAGE_HEADER *pRequest = NULL; ++ MBIM_COMMAND_DONE_T *pCmdDone = NULL; ++ int err; ++ unsigned int i, s; ++ ++ if (s_at_uuid) ++ return 0; /* already detected */ ++ ++ pRequest = mbim_compose_command(UUID_BASIC_CONNECT, ++ MBIM_CID_DEVICE_SERVICES, ++ MBIM_CID_CMD_TYPE_QUERY, NULL, 0); ++ err = mbim_send_command(pRequest, &pCmdDone); ++ if (err || !pCmdDone) { ++ mbim_free(pRequest); ++ mbim_free(pCmdDone); ++ /* Fall back to first candidate without detection */ ++ s_at_uuid = at_service_candidates[0].uuid; ++ s_at_cid = at_service_candidates[0].cid; ++ mbim_debug("detect_at_service: query failed, defaulting to %s", ++ at_service_candidates[0].description); ++ return 0; ++ } ++ ++ if (le32toh(pCmdDone->InformationBufferLength) >= sizeof(MBIM_DEVICE_SERVICES_INFO_T)) { ++ MBIM_DEVICE_SERVICES_INFO_T *info = ++ (MBIM_DEVICE_SERVICES_INFO_T *)pCmdDone->InformationBuffer; ++ uint32_t svc_count = le32toh(info->DeviceServicesCount); ++ ++ /* Walk candidate list; pick first one found in device services */ ++ for (i = 0; at_service_candidates[i].uuid && !s_at_uuid; i++) { ++ const UUID_T *want = (const UUID_T *)str2uuid_pub(at_service_candidates[i].uuid); ++ for (s = 0; s < svc_count && !s_at_uuid; s++) { ++ uint32_t offset = le32toh(info->DeviceServicesRefList[s].offset); ++ uint32_t size = le32toh(info->DeviceServicesRefList[s].size); ++ if (offset + size > le32toh(pCmdDone->InformationBufferLength)) ++ continue; ++ MBIM_DEVICE_SERVICE_ELEMENT_T *elem = ++ (MBIM_DEVICE_SERVICE_ELEMENT_T *) ++ ((uint8_t *)info + offset); ++ if (memcmp(elem->DeviceServiceId.uuid, want->uuid, 16) == 0) { ++ /* Check the required CID is in the CID list */ ++ uint32_t cid_count = le32toh(elem->CidCount); ++ uint32_t c; ++ for (c = 0; c < cid_count; c++) { ++ if (le32toh(elem->CidList[c]) == at_service_candidates[i].cid) { ++ s_at_uuid = at_service_candidates[i].uuid; ++ s_at_cid = at_service_candidates[i].cid; ++ mbim_debug("detect_at_service: found %s", ++ at_service_candidates[i].description); ++ break; ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ mbim_free(pRequest); ++ mbim_free(pCmdDone); ++ ++ if (!s_at_uuid) { ++ /* Nothing matched — fall back to first candidate */ ++ s_at_uuid = at_service_candidates[0].uuid; ++ s_at_cid = at_service_candidates[0].cid; ++ mbim_debug("detect_at_service: no match, defaulting to %s", ++ at_service_candidates[0].description); ++ } ++ ++ return 0; ++} ++ + static char s_atc_response[8192]; + int mbim_send_at_command(const char *atc_req, char **pp_atc_rsp) + { +@@ -566,7 +664,20 @@ + if (pp_atc_rsp) + *pp_atc_rsp = NULL; + printf("Send > %s\n", atc_req); +- pRequest = mbim_compose_command(uuid_qdu, 8, ++ /* Add \r terminator if not already present (required by AT command standard) */ ++ char at_buf[133]; ++ if (atc_len > 0 && atc_req[atc_len - 1] != '\r') { ++ if (atc_len + 1 >= sizeof(at_buf)) { ++ return -EINVAL; ++ } ++ memcpy(at_buf, atc_req, atc_len); ++ at_buf[atc_len++] = '\r'; ++ at_buf[atc_len] = '\0'; ++ atc_req = at_buf; ++ } ++ mbim_detect_at_service(); ++ printf("Using AT service: uuid=%s cid=%u\n", s_at_uuid, s_at_cid); ++ pRequest = mbim_compose_command(s_at_uuid, s_at_cid, + MBIM_CID_CMD_TYPE_SET, NULL, 4 + atc_len); + if (pRequest) + { +@@ -580,14 +691,20 @@ + + if (le32toh(pCmdDone->InformationBufferLength)) + { +- unsigned int i = 0; ++ uint32_t rsp_len = le32toh(pCmdDone->InformationBufferLength); + +- strncpy(s_atc_response, (char *)&pCmdDone->InformationBuffer[4], pCmdDone->InformationBufferLength - 4); +- s_atc_response[pCmdDone->InformationBufferLength - 4] = 0; +- +- printf("Recv < %s", s_atc_response); +- if (pp_atc_rsp) +- *pp_atc_rsp = s_atc_response; ++ if (rsp_len > 4) ++ { ++ uint32_t data_len = rsp_len - 4; ++ if (data_len >= sizeof(s_atc_response)) ++ data_len = sizeof(s_atc_response) - 1; ++ memcpy(s_atc_response, (char *)&pCmdDone->InformationBuffer[4], data_len); ++ s_atc_response[data_len] = '\0'; ++ ++ printf("Recv < %s", s_atc_response); ++ if (pp_atc_rsp) ++ *pp_atc_rsp = s_atc_response; ++ } + } + + // out: diff --git a/sing-box/Makefile b/sing-box/Makefile index c9ba4539..261c7565 100644 --- a/sing-box/Makefile +++ b/sing-box/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.13.11 -PKG_RELEASE:=15 +PKG_VERSION:=1.13.12 +PKG_RELEASE:=16 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?