mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:45:13 +08:00
🍉 Sync 2026-05-15 20:04:19
This commit is contained in:
1 parent
f4f587f762
commit
78d8d030c8
33 files changed
+1465
-714
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""
|
||||||
@@ -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 ""
|
||||||
@@ -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) };
|
||||||
|
|||||||
@@ -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:= \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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,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
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
config qbittorrent 'config'
|
||||||
|
option enabled '0'
|
||||||
|
option download_dir '/mnt/download'
|
||||||
|
option http_port '8080'
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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)?
|
||||||
|
|||||||
Reference in new issue
Block a user