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