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

This commit is contained in:
github-actions[bot] committed 2026-05-15 20:04:19 +08:00
1 parent f4f587f762
commit 78d8d030c8
33 files changed
+1465 -714

No files matched your search

+131
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))
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ipv6-neigh PKG_NAME:=ipv6-neigh
PKG_VERSION:=0.1.0 PKG_VERSION:=0.1.0
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_DEPENDS:=rust/host
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1
+58
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
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;
}
}
}
}
+32 -589
View File
@@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr}; use std::net::SocketAddr as StdSocketAddr;
use std::time::Instant; use std::time::Instant;
use futures::stream::StreamExt; use futures::stream::StreamExt;
@@ -7,13 +7,10 @@ use futures::stream::TryStreamExt;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use netlink_packet_core::NetlinkPayload; use netlink_packet_core::NetlinkPayload;
use netlink_packet_route::RouteNetlinkMessage; 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::NeighbourMessage;
use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState}; use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState};
use netlink_packet_route::route::RouteType; use netlink_packet_route::route::RouteType;
use rtnetlink::{Error, Handle, new_connection}; use rtnetlink::{Error, Handle, new_connection};
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
use std::num::NonZeroU32;
use tokio::time::{self, Duration}; use tokio::time::{self, Duration};
use clap::Parser; use clap::Parser;
@@ -21,18 +18,22 @@ use hickory_proto::rr::Name;
use hickory_proto::rr::TSigner; use hickory_proto::rr::TSigner;
use hickory_proto::rr::rdata::tsig::TsigAlgorithm; use hickory_proto::rr::rdata::tsig::TsigAlgorithm;
use netlink_sys::{AsyncSocket, SocketAddr}; use netlink_sys::{AsyncSocket, SocketAddr};
mod db; mod db;
mod filter;
mod iface;
mod op; mod op;
const RTNLGRP_NEIGH: u32 = 3; mod probe;
mod reconcile;
mod types;
const DEFAULT_TTL: u32 = 60; 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};
const fn nl_mgrp(group: u32) -> u32 { use probe::{probe_gua_keepalive, probe_registered_neighbours, prune_gua_keepalive, send_icmpv4_echo, send_icmpv6_echo};
if group > 31 { use reconcile::{do_delete_dns, process_new_neigh, prune_ula_for_mac, reconcile_dns};
panic!("use netlink_sys::Socket::add_membership() for this group"); use types::{
} DEFAULT_TTL, RTNLGRP_NEIGH, GuaKeepaliveEntry, Neigh, RegisteredEntry, nl_mgrp,
if group == 0 { 0 } else { 1 << (group - 1) } };
}
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[clap()] #[clap()]
@@ -74,149 +75,13 @@ struct Cli {
#[clap(long, default_value = "120")] #[clap(long, default_value = "120")]
keepalive_gua_interval: u64, keepalive_gua_interval: u64,
/// Maximum number of GUA addresses to keepalive-probe per host /// 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, keepalive_gua_per_host: usize,
/// Maximum number of ULA AAAA records to publish per host (oldest pruned first) /// Maximum number of ULA AAAA records to publish per host (oldest pruned first)
#[clap(long, default_value = "2")] #[clap(long, default_value = "2")]
max_ula_per_host: usize, 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 { fn should_skip_route_type(route_type: RouteType) -> bool {
matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local) matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local)
@@ -231,52 +96,7 @@ fn is_failed_state(state: NeighbourState) -> bool {
matches!(state, NeighbourState::Failed) matches!(state, NeighbourState::Failed)
} }
/// Enforce the per-host ULA AAAA limit. If `mac` already has `max` or more ULA AAAA #[tokio::main(flavor = "current_thread")]
/// 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]
async fn main() -> Result<(), ()> { async fn main() -> Result<(), ()> {
let args = Cli::parse(); 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 the DNS server to become available before sending any updates.
wait_for_dns_server(args.dns_server).await; 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); tokio::spawn(connection);
// Register router's own addresses in DNS // Register router's own addresses in DNS
@@ -334,8 +161,13 @@ async fn main() -> Result<(), ()> {
.await; .await;
} }
// Load DHCP leases (mac -> hostname) from ubus // Load DHCP leases (mac -> hostname) from ubus.
let mut leases = op::get_lease().unwrap_or_default(); // 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()); info!("loaded {} DHCP leases", leases.len());
// State cache: tracks (mac, ip_string) -> registered entry (hostname, last confirmed time, ifindex) // 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) = // (netlink event socket already set up above — reuse the same connection)
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);
// Start periodic probing task // Start periodic probing task
let probe_interval = args.probe_interval; let probe_interval = args.probe_interval;
@@ -705,254 +526,6 @@ async fn main() -> Result<(), ()> {
Ok(()) 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 { fn format_mac(mac: Vec<u8>) -> String {
let mut mac_str = String::new(); let mut mac_str = String::new();
@@ -1028,133 +601,3 @@ async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result<Vec<
Ok(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
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
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
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,
}
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-mosdns PKG_NAME:=luci-app-mosdns
PKG_VERSION:=1.7.2 PKG_VERSION:=1.7.3
PKG_RELEASE:=8 PKG_RELEASE:=9
LUCI_TITLE:=LuCI Support for mosdns LUCI_TITLE:=LuCI Support for mosdns
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
@@ -19,6 +19,8 @@ const callGetUpdateLog = rpc.declare({
return view.extend({ return view.extend({
handleUpdate() { handleUpdate() {
const statusMsg = E('p', { 'class': 'spinning' }, _('Please wait, this may take a few moments...'));
const logTextarea = E('textarea', { const logTextarea = E('textarea', {
'class': 'cbi-input-textarea', 'class': 'cbi-input-textarea',
'readonly': 'readonly', 'readonly': 'readonly',
@@ -33,7 +35,7 @@ return view.extend({
}, _('Close')); }, _('Close'));
ui.showModal(_('Updating Database...'), [ ui.showModal(_('Updating Database...'), [
E('p', { 'class': 'spinning' }, _('Please wait, this may take a few moments...')), statusMsg,
logTextarea, logTextarea,
E('div', { 'class': 'right' }, [ closeButton ]) E('div', { 'class': 'right' }, [ closeButton ])
]); ]);
@@ -45,14 +47,28 @@ return view.extend({
logTextarea.scrollTop = logTextarea.scrollHeight; logTextarea.scrollTop = logTextarea.scrollHeight;
if (res.log.match(/UPDATE_FINISHED/)) { if (res.log.match(/UPDATE_FINISHED/)) {
ui.hideModal(); statusMsg.textContent = _('Update success');
ui.addNotification(null, E('p', _('Update success')), 'info'); 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; return false;
} }
if (res.log.match(/Another update is already in progress/)) { if (res.log.match(/Another update is already in progress/)) {
ui.hideModal(); statusMsg.textContent = _('Another update is already in progress.');
ui.addNotification(null, E('p', _('Another update is already in progress.')), 'warn'); statusMsg.classList.remove('spinning');
statusMsg.style.color = '#ff9900';
closeButton.style.display = 'inline';
return false; return false;
} }
} }
@@ -70,12 +86,16 @@ return view.extend({
}); });
}, 1000); }, 1000);
} else { } else {
ui.hideModal(); statusMsg.textContent = res.error || _('Failed to start update.');
ui.addNotification(null, E('p', res.error || _('Failed to start update.')), 'error'); statusMsg.classList.remove('spinning');
statusMsg.style.color = '#ed4014';
closeButton.style.display = 'inline';
} }
}).catch(e => { }).catch(e => {
ui.hideModal(); statusMsg.textContent = _('Update failed: %s').format(e.message);
ui.addNotification(null, E('p', _('Update failed: %s').format(e.message)), 'error'); statusMsg.classList.remove('spinning');
statusMsg.style.color = '#ed4014';
closeButton.style.display = 'inline';
}); });
}, },
+36 -32
View File
@@ -74,7 +74,7 @@ msgstr ""
msgid "Aliyun Public DNS (DNS over QUIC)" msgid "Aliyun Public DNS (DNS over QUIC)"
msgstr "" 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." msgid "Another update is already in progress."
msgstr "" msgstr ""
@@ -86,7 +86,7 @@ msgstr ""
msgid "Auto Save Cache Interval" msgid "Auto Save Cache Interval"
msgstr "" 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 "" msgid ""
"Automatically update GeoIP and GeoSite databases as well as ad filtering " "Automatically update GeoIP and GeoSite databases as well as ad filtering "
"rules through scheduled tasks." "rules through scheduled tasks."
@@ -133,11 +133,11 @@ msgstr ""
msgid "Cache Dump" msgid "Cache Dump"
msgstr "" 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" msgid "Check And Update"
msgstr "" 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." msgid "Check And Update GeoData."
msgstr "" msgstr ""
@@ -163,7 +163,7 @@ msgstr ""
msgid "Clear logs" msgid "Clear logs"
msgstr "" 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" msgid "Close"
msgstr "" msgstr ""
@@ -246,7 +246,7 @@ msgstr ""
msgid "DNS redirect" msgid "DNS redirect"
msgstr "" 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" msgid "Database Update"
msgstr "" msgstr ""
@@ -276,7 +276,7 @@ msgstr ""
msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)"
msgstr "" 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" msgid "Enable Auto Database Update"
msgstr "" msgstr ""
@@ -318,35 +318,35 @@ msgstr ""
msgid "Error" msgid "Error"
msgstr "" 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" msgid "Every Day"
msgstr "" 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" msgid "Every Friday"
msgstr "" 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" msgid "Every Monday"
msgstr "" 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" msgid "Every Saturday"
msgstr "" 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" msgid "Every Sunday"
msgstr "" 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" msgid "Every Thursday"
msgstr "" 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" msgid "Every Tuesday"
msgstr "" 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" msgid "Every Wednesday"
msgstr "" msgstr ""
@@ -359,7 +359,7 @@ msgstr ""
msgid "Failed to clean logs." msgid "Failed to clean logs."
msgstr "" 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." msgid "Failed to start update."
msgstr "" msgstr ""
@@ -399,11 +399,11 @@ msgstr ""
msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS"
msgstr "" 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" msgid "Full"
msgstr "" 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." msgid "Full: includes all Countries and Private IP addresses."
msgstr "" msgstr ""
@@ -415,7 +415,7 @@ msgstr ""
msgid "GeoIP Tags" msgid "GeoIP Tags"
msgstr "" 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" msgid "GeoIP Type"
msgstr "" msgstr ""
@@ -427,7 +427,7 @@ msgstr ""
msgid "Geodata Update" msgid "Geodata Update"
msgstr "" 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" msgid "GitHub Proxy"
msgstr "" msgstr ""
@@ -503,11 +503,11 @@ msgstr ""
msgid "Listen Port" msgid "Listen Port"
msgstr "" 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" msgid "Little"
msgstr "" 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." msgid "Little: only include Mainland China and Private IP addresses."
msgstr "" msgstr ""
@@ -585,7 +585,7 @@ msgid ""
"Media DNS requests" "Media DNS requests"
msgstr "" 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..." msgid "Please wait, this may take a few moments..."
msgstr "" msgstr ""
@@ -641,7 +641,7 @@ msgstr ""
msgid "Save the cache locally and reload the cache dump on the next startup" msgid "Save the cache locally and reload the cache dump on the next startup"
msgstr "" 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..." msgid "Starting update..."
msgstr "" msgstr ""
@@ -721,32 +721,36 @@ msgstr ""
msgid "Unable to save contents: %s" msgid "Unable to save contents: %s"
msgstr "" 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" msgid "Update Cycle"
msgstr "" 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" msgid "Update GeoIP & GeoSite databases"
msgstr "" 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" msgid "Update Time"
msgstr "" 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 "" msgid ""
"Update data files with GitHub Proxy, leave blank to disable proxy downloads." "Update data files with GitHub Proxy, leave blank to disable proxy downloads."
msgstr "" 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" msgid "Update failed: %s"
msgstr "" 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" msgid "Update success"
msgstr "" 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..." msgid "Updating Database..."
msgstr "" msgstr ""
@@ -781,6 +785,6 @@ msgstr ""
msgid "Xinfeng Public DNS (114.114.115.115)" msgid "Xinfeng Public DNS (114.114.115.115)"
msgstr "" 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" msgid "https://gh-proxy.com"
msgstr "" msgstr ""
+38 -34
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-20 09:23+0800\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" "Last-Translator: sbwml <admin@cooluc.com>\n"
"Language-Team: Chinese\n" "Language-Team: Chinese\n"
"Language: zh_Hans\n" "Language: zh_Hans\n"
@@ -89,7 +89,7 @@ msgstr "阿里云公共 DNS(DNS over HTTPS)"
msgid "Aliyun Public DNS (DNS over QUIC)" msgid "Aliyun Public DNS (DNS over QUIC)"
msgstr "阿里云公共 DNS(DNS over QUIC)" msgstr "阿里云公共 DNS(DNS over QUIC)"
#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:55 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68
msgid "Another update is already in progress." msgid "Another update is already in progress."
msgstr "另一个更新正在进行中。" msgstr "另一个更新正在进行中。"
@@ -101,7 +101,7 @@ msgstr "Apple 域名解析优化"
msgid "Auto Save Cache Interval" msgid "Auto Save Cache Interval"
msgstr "自动保存缓存间隔(秒)" 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 "" msgid ""
"Automatically update GeoIP and GeoSite databases as well as ad filtering " "Automatically update GeoIP and GeoSite databases as well as ad filtering "
"rules through scheduled tasks." "rules through scheduled tasks."
@@ -150,11 +150,11 @@ msgstr "Bootstrap DNS 服务器用于解析您指定为上游的 DoH / DoT 解
msgid "Cache Dump" msgid "Cache Dump"
msgstr "自动保存缓存" 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" msgid "Check And Update"
msgstr "检查并更新" 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." msgid "Check And Update GeoData."
msgstr "检查并更新 GeoData 数据库。" msgstr "检查并更新 GeoData 数据库。"
@@ -180,7 +180,7 @@ msgstr "思科公共 DNS(208.67.222.222)"
msgid "Clear logs" msgid "Clear logs"
msgstr "清空日志" 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" msgid "Close"
msgstr "关闭" msgstr "关闭"
@@ -264,7 +264,7 @@ msgstr "DNS 查询请求并发数,允许同时发起请求的上游 DNS 服务
msgid "DNS redirect" msgid "DNS redirect"
msgstr "DNS 重定向" 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" msgid "Database Update"
msgstr "数据库更新" msgstr "数据库更新"
@@ -295,7 +295,7 @@ msgstr ""
msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)"
msgstr "DoH/TCP/DoT 连接复用空闲保持时间(默认 30 秒)" 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" msgid "Enable Auto Database Update"
msgstr "启用自动更新" msgstr "启用自动更新"
@@ -337,35 +337,35 @@ msgstr "填写需要导出的 GeoSite.dat 类别条目,允许添加多个标
msgid "Error" msgid "Error"
msgstr "错误" 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" msgid "Every Day"
msgstr "每天" 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" msgid "Every Friday"
msgstr "每周五" 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" msgid "Every Monday"
msgstr "每周一" 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" msgid "Every Saturday"
msgstr "每周六" 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" msgid "Every Sunday"
msgstr "每周日" 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" msgid "Every Thursday"
msgstr "每周四" 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" msgid "Every Tuesday"
msgstr "每周二" 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" msgid "Every Wednesday"
msgstr "每周三" msgstr "每周三"
@@ -378,7 +378,7 @@ msgstr "导出目录:/var/mosdns"
msgid "Failed to clean logs." msgid "Failed to clean logs."
msgstr "清理日志失败:%s" 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." msgid "Failed to start update."
msgstr "启动更新失败" msgstr "启动更新失败"
@@ -418,11 +418,11 @@ msgstr "强制将所有本地 DNS 查询重定向到 MosDNS,即 DNS 劫持。"
msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS"
msgstr "将 Dnsmasq 域名解析请求转发到 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" msgid "Full"
msgstr "全量" 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." msgid "Full: includes all Countries and Private IP addresses."
msgstr "全量:包含所有国家和私有 IP 地址。" msgstr "全量:包含所有国家和私有 IP 地址。"
@@ -434,7 +434,7 @@ msgstr "GeoData 导出"
msgid "GeoIP Tags" msgid "GeoIP Tags"
msgstr "GeoIP 标签" 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" msgid "GeoIP Type"
msgstr "GeoIP 类型" msgstr "GeoIP 类型"
@@ -446,7 +446,7 @@ msgstr "GeoSite 标签"
msgid "Geodata Update" msgid "Geodata Update"
msgstr "更新数据库" 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" msgid "GitHub Proxy"
msgstr "GitHub 代理" msgstr "GitHub 代理"
@@ -463,7 +463,7 @@ msgstr "谷歌公共 DNS(8.8.8.8)"
#: luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json:3 #: luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json:3
msgid "Grant UCI access for luci-app-mosdns" 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 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:20
msgid "Grey Lists" msgid "Grey Lists"
@@ -522,11 +522,11 @@ msgstr "监听地址"
msgid "Listen Port" msgid "Listen Port"
msgstr "监听端口" 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" msgid "Little"
msgstr "轻量" 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." msgid "Little: only include Mainland China and Private IP addresses."
msgstr "轻量:仅包含中国大陆和私有 IP 地址。" msgstr "轻量:仅包含中国大陆和私有 IP 地址。"
@@ -609,7 +609,7 @@ msgstr ""
"请提供您在访问国外网站时使用的 IP 地址,这个 IP 子网(0/24)将用作 远程 / 流" "请提供您在访问国外网站时使用的 IP 地址,这个 IP 子网(0/24)将用作 远程 / 流"
"媒体 DNS 请求的 ECS 地址" "媒体 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..." msgid "Please wait, this may take a few moments..."
msgstr "请稍候,这可能需要一点时间..." msgstr "请稍候,这可能需要一点时间..."
@@ -667,7 +667,7 @@ msgstr "规则列表"
msgid "Save the cache locally and reload the cache dump on the next startup" msgid "Save the cache locally and reload the cache dump on the next startup"
msgstr "保存缓存到本地文件,以供下次启动时重新载入使用" 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..." msgid "Starting update..."
msgstr "" msgstr ""
@@ -751,32 +751,36 @@ msgstr "火山引擎公共 DNS(180.184.2.2)"
msgid "Unable to save contents: %s" msgid "Unable to save contents: %s"
msgstr "无法保存内容:%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" msgid "Update Cycle"
msgstr "更新周期" 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" msgid "Update GeoIP & GeoSite databases"
msgstr "更新广告规则、GeoIP & GeoSite 数据库" 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" msgid "Update Time"
msgstr "更新时间" 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 "" msgid ""
"Update data files with GitHub Proxy, leave blank to disable proxy downloads." "Update data files with GitHub Proxy, leave blank to disable proxy downloads."
msgstr "通过 GitHub 代理更新数据文件,留空则禁用代理下载。" 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" msgid "Update failed: %s"
msgstr "更新失败: %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" msgid "Update success"
msgstr "更新成功" 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..." msgid "Updating Database..."
msgstr "更新数据库..." msgstr "更新数据库..."
@@ -815,6 +819,6 @@ msgstr "信风公共 DNS(114.114.114.114)"
msgid "Xinfeng Public DNS (114.114.115.115)" msgid "Xinfeng Public DNS (114.114.115.115)"
msgstr "信风公共 DNS(114.114.115.115)" msgstr "信风公共 DNS(114.114.115.115)"
#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:123 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143
msgid "https://gh-proxy.com" msgid "https://gh-proxy.com"
msgstr "" msgstr ""
+12 -10
View File
@@ -158,7 +158,7 @@ function update_adlist() {
print("\x1b[1;31mRules download failed.\n"); print("\x1b[1;31mRules download failed.\n");
exec_sys(`rm -rf "${ad_tmpdir}"`); exec_sys(`rm -rf "${ad_tmpdir}"`);
unlink(lock_file); unlink(lock_file);
exit(1); die("Rules download failed.");
} else { } else {
if (has_update) { if (has_update) {
mkdir('/etc/mosdns/rule/adlist', 0755); mkdir('/etc/mosdns/rule/adlist', 0755);
@@ -183,7 +183,7 @@ function update_geodat() {
let v2dat_dir = '/usr/share/v2ray'; let v2dat_dir = '/usr/share/v2ray';
let tmp_res = exec_sys('mktemp -d'); 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; let tmpdir = tmp_res.stdout;
exec_sys(`mkdir -p "${v2dat_dir}"`); exec_sys(`mkdir -p "${v2dat_dir}"`);
@@ -194,7 +194,7 @@ function update_geodat() {
print(`Downloading ${geoip_url}.sha256sum\n`); print(`Downloading ${geoip_url}.sha256sum\n`);
stdout.flush(); stdout.flush();
if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geoip.dat.sha256sum" "${geoip_url}.sha256sum"`).code !== 0) { 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]; 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`); print(`Downloading ${geoip_url}\n`);
stdout.flush(); stdout.flush();
if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geoip.dat" "${geoip_url}"`).code !== 0) { 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]; let sum_downloaded = split(exec_sys(`sha256sum "${tmpdir}/geoip.dat"`).stdout, /[ \t\n]+/)[0];
if (sum_downloaded !== geoip_sum_remote) { if (sum_downloaded !== geoip_sum_remote) {
print("\x1b[1;31mgeoip.dat checksum error\n"); 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; geoip_updated = true;
} }
@@ -227,7 +227,7 @@ function update_geodat() {
print(`Downloading ${geosite_url}.sha256sum\n`); print(`Downloading ${geosite_url}.sha256sum\n`);
stdout.flush(); stdout.flush();
if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geosite.dat.sha256sum" "${geosite_url}.sha256sum"`).code !== 0) { 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]; 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`); print(`Downloading ${geosite_url}\n`);
stdout.flush(); stdout.flush();
if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geosite.dat" "${geosite_url}"`).code !== 0) { 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]; let sum_downloaded = split(exec_sys(`sha256sum "${tmpdir}/geosite.dat"`).stdout, /[ \t\n]+/)[0];
if (sum_downloaded !== geosite_sum_remote) { if (sum_downloaded !== geosite_sum_remote) {
print("\x1b[1;31mgeosite.dat checksum error\n"); 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; geosite_updated = true;
} }
@@ -332,9 +332,11 @@ switch (action) {
print("UPDATE_FINISHED\n"); print("UPDATE_FINISHED\n");
stdout.flush(); stdout.flush();
} catch (e) { } catch (e) {
print("Update failed: " + e + "\n"); print("\x1b[1;31mUpdate failed: " + e + "\n\x1b[0m");
print("UPDATE_FINISHED\n"); print("UPDATE_EXITED\n");
stdout.flush(); stdout.flush();
unlink('/var/lock/mosdns_update.lock');
exit(1);
} }
unlink('/var/lock/mosdns_update.lock'); unlink('/var/lock/mosdns_update.lock');
break; break;
@@ -128,7 +128,7 @@ const methods = {
return { success: false, error: 'Another update is already in progress.' }; return { success: false, error: 'Another update is already in progress.' };
} }
exec_sys('echo "" > /var/log/mosdns_update.log'); 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 }; return { success: true };
} catch (e) { } catch (e) {
return { success: false, error: String(e) }; return { success: false, error: String(e) };
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.5.11 PKG_VERSION:=26.5.11
PKG_RELEASE:=123 PKG_RELEASE:=124
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
+1 -1
View File
@@ -1205,7 +1205,7 @@ function to_move(app_name,file)
} }
end 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 if flag == 0 then
sys.call("/etc/init.d/passwall stop") sys.call("/etc/init.d/passwall stop")
end end
@@ -1054,7 +1054,7 @@ socks_node_switch() {
[ -s "$pf" ] && kill -9 "$(head -n1 "$pf")" >/dev/null 2>&1 [ -s "$pf" ] && kill -9 "$(head -n1 "$pf")" >/dev/null 2>&1
done 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 for prefix in "" "HTTP_" "HTTP2"; do
rm -rf "$TMP_PATH/${prefix}SOCKS_${flag}"* rm -rf "$TMP_PATH/${prefix}SOCKS_${flag}"*
done 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}/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 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 rm -f ${LOCK_PATH}/${CONFIG}_tasks.lock
} }
@@ -1941,8 +1941,8 @@ stop() {
kill -9 "$pid" >/dev/null 2>&1 kill -9 "$pid" >/dev/null 2>&1
fi fi
done done
pgrep -f "sleep.*(6s|9s|58s)" | xargs kill -9 >/dev/null 2>&1 busybox 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 -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 V2RAY_LOCATION_ASSET
unset XRAY_LOCATION_ASSET unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
@@ -8,7 +8,7 @@ listen_port=$2
server_address=$3 server_address=$3
server_port=$4 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 exit 0
} }
@@ -38,7 +38,7 @@ while [ 1 -eq 1 ]; do
# 检查是否超过最大重启次数 # 检查是否超过最大重启次数
[ "$restart_count" -ge "$MAX_RESTART_COUNT" ] && continue [ "$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)) restart_count=$((restart_count + 1))
echo "$restart_count" > "$stats_file" echo "$restart_count" > "$stats_file"
#echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log #echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log
@@ -6,7 +6,7 @@ APP_FILE=${APP_PATH}/app.sh
flag=0 flag=0
check_process() { 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 sleep 6s
done done
} }
@@ -63,7 +63,7 @@ test_node() {
# 结束 SS 插件进程 # 结束 SS 插件进程
local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid" 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 [ -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}*.* rm -rf /tmp/etc/${CONFIG}/test_node_${node_id}*.*
if [ "${_proxy_status}" -eq 200 ]; then if [ "${_proxy_status}" -eq 200 ]; then
return 0 return 0
@@ -66,7 +66,7 @@ url_test_node() {
# 结束 SS 插件进程 # 结束 SS 插件进程
local pid_file="/tmp/etc/${CONFIG}/url_test_${node_id}_plugin.pid" 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 [ -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}*.* rm -rf /tmp/etc/${CONFIG}/*url_test_${node_id}*.*
} }
echo $result echo $result
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Status Pages LUCI_TITLE:=LuCI Status Pages
LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo LUCI_DEPENDS:=+luci-base +libiwinfo +rpcd-mod-iwinfo
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_BUILD_DEPENDS:=iwinfo PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
@@ -230,6 +230,40 @@ function addBoardNetworkPorts(knownPorts, seenPorts, role, entry, mapping, board
addResolvedPort(knownPorts, seenPorts, role, value, mapping, board, swstate); 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) function resolveVLANChain(ifname, bridges, mapping)
{ {
while (!mapping[ifname]) { while (!mapping[ifname]) {
@@ -630,10 +664,12 @@ return baseclass.extend({
L.resolveDefault(fs.read('/etc/board.json'), '{}'), L.resolveDefault(fs.read('/etc/board.json'), '{}'),
firewall.getZones(), firewall.getZones(),
network.getNetworks(), network.getNetworks(),
network.getDevices(),
uci.load('network') uci.load('network')
]).then((data) => { ]).then((data) => {
const builtinPorts = data[0] || []; const builtinPorts = data[0] || [];
const board = JSON.parse(data[1] || '{}'); const board = JSON.parse(data[1] || '{}');
const devices = data[4] || [];
const allPorts = new Set(); const allPorts = new Set();
const swstate = {}; const swstate = {};
const tasks = []; const tasks = [];
@@ -643,16 +679,18 @@ return baseclass.extend({
allPorts.add(port.device); allPorts.add(port.device);
}); });
if (allPorts.size === 0 && board.network) { forEachBoardNetworkEntry(board, function(role, entry) {
['lan', 'wan'].forEach((role) => { parseBoardPortList(entry.ports).forEach((p) => allPorts.add(p));
if (board.network[role]) { parseBoardPortList(entry.ifname).forEach((p) => allPorts.add(p));
if (Array.isArray(board.network[role].ports))
board.network[role].ports.forEach((p) => allPorts.add(p)); if (isString(entry.device))
else if (board.network[role].device) allPorts.add(entry.device);
allPorts.add(board.network[role].device); });
}
devices.forEach((dev) => {
if (isEnumeratedEthernetPort(dev))
allPorts.add(dev.getName());
}); });
}
if (L.isObject(board) && L.isObject(board.switch)) { if (L.isObject(board) && L.isObject(board.switch)) {
for (const switchName in board.switch) { for (const switchName in board.switch) {
@@ -689,9 +727,10 @@ return baseclass.extend({
render(data) { render(data) {
const board = JSON.parse(data[1]), const board = JSON.parse(data[1]),
swstate = data[5] || {}, devices = data[4] || [],
swstate = data[6] || {},
port_map = buildInterfaceMapping(data[2], data[3], board), port_map = buildInterfaceMapping(data[2], data[3], board),
pseMap = data[6] || {}; pseMap = data[7] || {};
let known_ports = []; let known_ports = [];
const seenPorts = {}; const seenPorts = {};
const vlanMap = {}; const vlanMap = {};
@@ -705,14 +744,11 @@ return baseclass.extend({
}, []); }, []);
} }
if (L.isObject(board) && L.isObject(board.network)) { forEachBoardNetworkEntry(board, function(role, entry) {
for (let k = 'lan'; k != null; k = (k == 'lan') ? 'wan' : null) { addBoardNetworkPorts(known_ports, seenPorts, role, entry, vlanMap, board, swstate);
if (!L.isObject(board.network[k])) });
continue;
addBoardNetworkPorts(known_ports, seenPorts, k, board.network[k], vlanMap, board, swstate); addSystemEthernetPorts(known_ports, seenPorts, devices, vlanMap, board, swstate);
}
}
if (!known_ports.length) if (!known_ports.length)
return null; return null;
@@ -52,7 +52,8 @@
"description": "Grant access to port status display", "description": "Grant access to port status display",
"read": { "read": {
"ubus": { "ubus": {
"luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ] "luci": [ "getBuiltinEthernetPorts", "getSwconfigPortState" ],
"luci-rpc": [ "getNetworkDevices" ]
} }
} }
}, },
+2 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo-alpha PKG_NAME:=mihomo-alpha
PKG_VERSION:=2026.05.08 PKG_VERSION:=2026.05.08
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
@@ -22,6 +22,7 @@ PKG_BUILD_VERSION:=alpha-35d5d4e4
PKG_BUILD_TIME:=$(shell date -u -Iseconds) PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo 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_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor GO_PKG_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
+2 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo-meta PKG_NAME:=mihomo-meta
PKG_VERSION:=1.19.24 PKG_VERSION:=1.19.24
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
@@ -22,6 +22,7 @@ PKG_BUILD_VERSION:=v1.19.24
PKG_BUILD_TIME:=$(shell date -u -Iseconds) PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo 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_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor GO_PKG_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
+62
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))
+6
View File
@@ -0,0 +1,6 @@
config qbittorrent 'config'
option enabled '0'
option download_dir '/mnt/download'
option http_port '8080'
+79
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
}
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=qmbimat PKG_NAME:=qmbimat
PKG_VERSION:=0.0.1 PKG_VERSION:=0.0.1
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru> PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
PKG_LICENSE:=LICENSE PKG_LICENSE:=LICENSE
@@ -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:
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box PKG_NAME:=sing-box
PKG_VERSION:=1.13.11 PKG_VERSION:=1.13.12
PKG_RELEASE:=15 PKG_RELEASE:=16
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?