🍉 Sync 2026-05-26 21:02:29

This commit is contained in:
github-actions[bot] 2026-05-26 21:02:29 +08:00
parent 755c7e3679
commit 8ab0ff5223
9 changed files with 142 additions and 7 deletions

2
ipv6-neigh/Cargo.lock generated
View File

@ -395,7 +395,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hickory-proto"
version = "0.27.0-alpha.1"
source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=c5e29b9da8334a14961a6326ad04e25397205430#c5e29b9da8334a14961a6326ad04e25397205430"
source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=ee7ba64757eca61d86f323ef992eba482605d7c3#ee7ba64757eca61d86f323ef992eba482605d7c3"
dependencies = [
"bitflags",
"data-encoding",

View File

@ -13,7 +13,7 @@ futures = "0.3.32"
netlink-packet-route = {version = "0.30" }
netlink-packet-core= {version ="0.8"}
clap = { version = "4.6", default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] }
hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "c5e29b9da8334a14961a6326ad04e25397205430", features = ["dnssec-ring"] }
hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "ee7ba64757eca61d86f323ef992eba482605d7c3", features = ["dnssec-ring"] }
ubus = "0.1.7"
serde_json = "1.0.149"
log = "0.4"

View File

@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ipv6-neigh
PKG_VERSION:=0.1.0
PKG_RELEASE:=13
PKG_RELEASE:=14
PKG_BUILD_DEPENDS:=rust/host
PKG_BUILD_PARALLEL:=1

View File

@ -518,6 +518,133 @@ async fn main() -> Result<(), ()> {
private_subnet_v6,
)
.await;
// Recover kernel orphans: neighbours that are REACHABLE in the kernel
// but absent from `registered` and DNS. This happens when a netlink
// NewNeighbour event is lost (receive-buffer overflow) or when a device
// transitions to REACHABLE during the startup race window.
// reconcile_dns's AXFR pass cannot catch these because the record is
// also absent from DNS, so no probe is ever triggered for them.
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await {
// Build a set of IPs that are currently REACHABLE in the kernel,
// used below to detect registered entries the kernel has marked FAILED.
let reachable_ips: std::collections::HashSet<String> = neighbours
.iter()
.filter(|n| n.state == NeighbourState::Reachable)
.map(|n| inet_to_string(&n.inet))
.collect();
for neigh in &neighbours {
if should_skip_neigh(neigh) {
continue;
}
if is_failed_state(neigh.state) {
// Kernel confirms FAILED — remove from registered + DNS if present.
// Mirrors the real-time NewNeighbour(FAILED) handler but catches
// cases where that event was lost.
let ip_str = inet_to_string(&neigh.inet);
let key_opt: Option<(String, String)> = leases
.get(&neigh.mac)
.map(|h| (h.clone(), ip_str.clone()))
.or_else(|| {
registered
.keys()
.find(|(_, ip)| ip == &ip_str)
.cloned()
});
if let Some(key) = key_opt {
if let Some(entry) = registered.remove(&key) {
debug!("reconcile neigh: kernel FAILED {} -> {:?}", entry.hostname, neigh.inet);
if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await {
registered.insert(key, entry);
}
}
}
continue;
}
if neigh.state != NeighbourState::Reachable {
continue;
}
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
if !active_prefixes.is_empty()
&& !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p))
{
continue;
}
if is_gua_ipv6(addr) && private_subnet_v6 {
continue;
}
}
let ip_str = inet_to_string(&neigh.inet);
let Some(hostname) = leases.get(&neigh.mac) else {
continue;
};
let key = (hostname.clone(), ip_str);
if let Some(entry) = registered.get_mut(&key) {
// Already tracked — refresh to suppress spurious probes.
entry.last_confirmed = Instant::now();
entry.ifindex = neigh.ifindex;
} else {
debug!("reconcile neigh: kernel orphan {} -> {:?}", hostname, neigh.inet);
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
if if_ipv6_in_private_subnet(addr) {
prune_ula_for_host(
hostname,
max_ula_per_host,
&mut registered,
&updater,
)
.await;
}
}
if process_new_neigh(neigh, &updater, &leases, private_subnet_v6).await {
registered.insert(
key,
RegisteredEntry {
hostname: hostname.clone(),
last_confirmed: Instant::now(),
ifindex: neigh.ifindex,
},
);
}
}
}
// Sweep registered entries whose IP the kernel has evicted entirely
// (not present in the dump at all, not even as FAILED) AND that haven't
// been confirmed recently. These are devices whose FAILED event was
// lost and which the kernel has since garbage-collected from its table.
// Only act if the entry is old enough (> 2× probe interval) to avoid
// racing with normal STALE→REACHABLE transitions.
let stale_cutoff = Duration::from_secs(probe_interval.saturating_mul(2));
let now = Instant::now();
let to_remove: Vec<(String, String)> = registered
.iter()
.filter(|((_hostname, ip_str), entry)| {
!reachable_ips.contains(ip_str.as_str())
&& now.duration_since(entry.last_confirmed) > stale_cutoff
})
.map(|(k, _)| k.clone())
.collect();
for key in to_remove {
if let Some(entry) = registered.remove(&key) {
let ip_str = &key.1;
let inet = if let Ok(addr) = ip_str.parse::<std::net::Ipv6Addr>() {
NeighbourAddress::Inet6(addr)
} else if let Ok(addr) = ip_str.parse::<std::net::Ipv4Addr>() {
NeighbourAddress::Inet(addr)
} else {
registered.insert(key, entry);
continue;
};
debug!("reconcile neigh: evicted {} -> {}", entry.hostname, ip_str);
if !do_delete_dns(&entry.hostname, &inet, &updater).await {
registered.insert(key, entry);
}
}
}
}
}
_ = gua_keepalive_timer.tick() => {
if !keepalive_gua || keepalive_gua_interval == 0 {

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.5.20
PKG_RELEASE:=137
PKG_RELEASE:=138
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -947,6 +947,7 @@ function restore_backup()
fp:write(decoded)
fp:close()
if chunk_index + 1 == total_chunks then
uci:revert(appname)
luci.sys.call("echo '' > /tmp/log/passwall.log")
api.log(" * PassWall 配置文件上传成功…")
local temp_dir = '/tmp/passwall_bak'

View File

@ -132,12 +132,14 @@ function base64Encode(text)
end
function UrlEncode(szText)
if type(szText) ~= "string" then return "" end
return szText:gsub("([^%w%-_%.%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
function UrlDecode(szText)
if type(szText) ~= "string" then return "" end
return szText and szText:gsub("%+", " "):gsub("%%(%x%x)", function(h)
return string.char(tonumber(h, 16))
end) or nil

View File

@ -66,10 +66,10 @@ local function build_common(node)
local ech_opts = node["ech-opts"]
if ech_opts and ech_opts.enable == true then
if ech_opts.config then
o.tls.ech = ech_opts.config
elseif ech_opts["query-server-name"] then
if ech_opts["query-server-name"] then
o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query"
elseif ech_opts.config then
o.tls.ech = ech_opts.config
end
end

View File

@ -683,6 +683,11 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.tls = "0"
end
if info.ech and info.ech ~= "" then
result.ech = "1"
result.ech_config = info.ech
end
result.tcp_fast_open = info.tfo
info.fm = (info.fm and info.fm ~= "") and UrlDecode(info.fm) or nil