🍉 Sync 2026-05-15 20:04:19

This commit is contained in:
github-actions[bot] 2026-05-15 20:04:19 +08:00
parent 895988c355
commit 9694ef054a
33 changed files with 1466 additions and 715 deletions

131
haproxy/Makefile Normal file
View File

@ -0,0 +1,131 @@
#
# Copyright (C) 2010-2016 OpenWrt.org
# Copyright (C) 2009-2016 Thomas Heil <heil@terminal-consulting.de>
# Copyright (C) 2018 Christian Lachner <gladiac@gmail.com>
#
# 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 <heil@terminal-consulting.de>, \
Christian Lachner <gladiac@gmail.com>
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))

View File

@ -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

58
ipv6-neigh/src/filter.rs Normal file
View File

@ -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))
}

145
ipv6-neigh/src/iface.rs Normal file
View File

@ -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<LanPrefix> {
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;
}
}
}
}

View File

@ -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<String, String>, 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::<Ipv6Addr>().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::<Ipv6Addr>().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<String, String> = 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<String, String>,
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<String, String> = 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::<IpAddr>() {
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::<IpAddr>() {
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::<IpAddr>() {
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::<Ipv6Addr>() {
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::<Ipv4Addr>() {
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<String, Vec<GuaKeepaliveEntry>>,
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<usize> = (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<String, Vec<GuaKeepaliveEntry>>,
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<u8>) -> String {
let mut mac_str = String::new();
@ -1028,133 +601,3 @@ async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result<Vec<
Ok(vec)
}
/// Check whether an address message is deprecated (preferred lifetime expired).
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.
async fn get_active_lan_prefixes(handle: Handle, iface: &str, private_subnet_v6: bool) -> Vec<LanPrefix> {
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;
}
}
}
}

131
ipv6-neigh/src/probe.rs Normal file
View File

@ -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::<Ipv6Addr>() {
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::<Ipv4Addr>() {
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<String, Vec<GuaKeepaliveEntry>>,
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<usize> = (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<String, Vec<GuaKeepaliveEntry>>,
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()
});
}

242
ipv6-neigh/src/reconcile.rs Normal file
View File

@ -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<String, String>,
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::<Ipv6Addr>().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::<Ipv6Addr>().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<String, String>,
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<String, String> = 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::<IpAddr>() {
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::<IpAddr>() {
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::<IpAddr>() {
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),
}
}
}

48
ipv6-neigh/src/types.rs Normal file
View File

@ -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,
}

View File

@ -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

View File

@ -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';
});
},

View File

@ -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 ""

View File

@ -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 <admin@cooluc.com>\n"
"Language-Team: Chinese\n"
"Language: zh_Hans\n"
@ -89,7 +89,7 @@ msgstr "阿里云公共 DNSDNS over HTTPS"
msgid "Aliyun Public DNS (DNS over QUIC)"
msgstr "阿里云公共 DNSDNS 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 "思科公共 DNS208.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 "谷歌公共 DNS8.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 "火山引擎公共 DNS180.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 "信风公共 DNS114.114.114.114"
msgid "Xinfeng Public DNS (114.114.115.115)"
msgstr "信风公共 DNS114.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 ""

View File

@ -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;

View File

@ -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) };

View File

@ -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:= \

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -52,7 +52,8 @@
"description": "Grant access to port status display",
"read": {
"ubus": {
"luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ]
"luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ],
"luci-rpc": [ "getNetworkDevices" ]
}
}
},

View File

@ -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

View File

@ -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

62
qbittorrent/Makefile Normal file
View File

@ -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))

View File

@ -0,0 +1,6 @@
config qbittorrent 'config'
option enabled '0'
option download_dir '/mnt/download'
option http_port '8080'

View File

@ -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
}

View File

@ -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

View File

@ -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 <shevlakov@132lan.ru>
PKG_LICENSE:=LICENSE

View File

@ -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:

View File

@ -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)?