diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index 9639aed5..d48afe03 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -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", diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index b30f3b81..09479045 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -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" diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index 9143d4d6..f33930da 100644 --- a/ipv6-neigh/Makefile +++ b/ipv6-neigh/Makefile @@ -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 diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index 0c71eec8..5d4fd519 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -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 = 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::() { + NeighbourAddress::Inet6(addr) + } else if let Ok(addr) = ip_str.parse::() { + 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 { diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 7540dbf7..00efa2d1 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -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:= \ diff --git a/luci-app-passwall/luasrc/controller/passwall.lua b/luci-app-passwall/luasrc/controller/passwall.lua index 6915316d..5dca7ad1 100644 --- a/luci-app-passwall/luasrc/controller/passwall.lua +++ b/luci-app-passwall/luasrc/controller/passwall.lua @@ -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' diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index fb54b982..10886cf0 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -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 diff --git a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua index e94afd64..5ad9444b 100644 --- a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua +++ b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua @@ -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 diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index f38a0e94..9e140544 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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