From e2f4c3d0b5d98b5e841d22d7f9dca89d137bf225 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 May 2026 20:31:49 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=A4=20Sync=202026-05-08=2020:31:49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- geoview/Makefile | 4 +- hickory-dns/Makefile | 2 +- .../files/etc/hickory-dns/forwarder.toml | 1 + .../files/etc/hotplug.d/iface/99-hickory-dns | 22 ++ hickory-dns/files/etc/init.d/hickory-dns | 11 +- ipv6-neigh/Cargo.lock | 1 + ipv6-neigh/Cargo.toml | 1 + ipv6-neigh/Makefile | 4 +- ipv6-neigh/files/etc/init.d/ipv6-neigh | 17 - ipv6-neigh/src/db.rs | 95 ++++- ipv6-neigh/src/main.rs | 337 ++++++++++++++++-- ipv6-neigh/src/op.rs | 47 +++ .../resources/view/dockerman/events.js | 39 +- .../resources/view/dockerman/overview.js | 150 +++++++- luci-theme-design/Makefile | 2 +- .../htdocs/luci-static/design/css/style.css | 39 ++ .../luci-static/resources/menu-design.js | 26 +- .../ucode/controller/design_theme.uc | 7 +- mihomo-alpha/Makefile | 8 +- ooniprobe/Makefile | 4 +- qminfo/Makefile | 3 +- qminfo/src/qminfo.c | 10 +- 22 files changed, 738 insertions(+), 92 deletions(-) create mode 100644 hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns delete mode 100644 ipv6-neigh/files/etc/init.d/ipv6-neigh diff --git a/geoview/Makefile b/geoview/Makefile index c0eaf538..d608d954 100644 --- a/geoview/Makefile +++ b/geoview/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=geoview -PKG_VERSION:=0.2.5 -PKG_RELEASE:=1 +PKG_VERSION:=0.2.6 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)? diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index 6a912e8b..b28ba066 100644 --- a/hickory-dns/Makefile +++ b/hickory-dns/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hickory-dns PKG_VERSION:=0.26.1 -PKG_RELEASE:=21 +PKG_RELEASE:=22 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)? diff --git a/hickory-dns/files/etc/hickory-dns/forwarder.toml b/hickory-dns/files/etc/hickory-dns/forwarder.toml index 6d147dae..98729bdc 100644 --- a/hickory-dns/files/etc/hickory-dns/forwarder.toml +++ b/hickory-dns/files/etc/hickory-dns/forwarder.toml @@ -33,6 +33,7 @@ file = "/etc/hickory-dns/0.zone" [[zones]] zone = "lan" zone_type = "Primary" +axfr_policy = "AllowAll" [zones.stores] type = "sqlite" diff --git a/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns b/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns new file mode 100644 index 00000000..b735e7c6 --- /dev/null +++ b/hickory-dns/files/etc/hotplug.d/iface/99-hickory-dns @@ -0,0 +1,22 @@ +#!/bin/sh + +. /lib/functions.sh +IF_TO_WATCH="wan_6" +SERVICE="hickory-dns" +DELAY=5 + +## Restart service on ifup or ifupdate for the watched interface +if { [ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ]; } && [ "$INTERFACE" = "$IF_TO_WATCH" ]; then + logger -t hickory-hotplug "Interface ${IF_TO_WATCH} ${ACTION} - ${SERVICE} restart in ${DELAY}s" + + ( + sleep ${DELAY} + + if /etc/init.d/${SERVICE} enabled; then + /etc/init.d/${SERVICE} restart >/dev/null 2>&1 + logger -t hickory-hotplug "${SERVICE} restarted successfully after ${IF_TO_WATCH} ${ACTION}" + else + logger -t hickory-hotplug "${SERVICE} not enabled (skipped)" + fi + ) & +fi \ No newline at end of file diff --git a/hickory-dns/files/etc/init.d/hickory-dns b/hickory-dns/files/etc/init.d/hickory-dns index 52bba064..3a8fba9c 100755 --- a/hickory-dns/files/etc/init.d/hickory-dns +++ b/hickory-dns/files/etc/init.d/hickory-dns @@ -7,6 +7,7 @@ STOP=51 PROG=/usr/bin/hickory-dns CONF=/etc/hickory-dns/forwarder.toml TSIG_KEY=/etc/hickory-dns/update.key +NEIGH_PROG=/usr/bin/ipv6-neigh generate_tsig_key() { [ -f "$TSIG_KEY" ] && return 0 @@ -27,7 +28,11 @@ start_service() { procd_set_param stderr 1 procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" procd_close_instance hickory-dns - # Journal was wiped above; restart ipv6-neigh so it re-registers all - # dynamic DNS entries from the current neighbour table. - /etc/init.d/ipv6-neigh restart + + procd_open_instance ipv6-neigh + procd_set_param command $NEIGH_PROG --private-subnet-v4 -d "[::1]:53" -z lan + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" + procd_close_instance ipv6-neigh } diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index 7a232d7e..5c14d5c7 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -558,6 +558,7 @@ dependencies = [ "env_logger", "futures", "hickory-proto", + "libc", "log", "netlink-packet-core", "netlink-packet-route", diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index bc7294fc..e9f5bcd4 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -19,6 +19,7 @@ serde_json = "1.0.108" log = "0.4" env_logger = "0.11" socket2 = "0.5" +libc = "0.2" [profile.release] opt-level = 3 diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index 76563328..c522d4fc 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:=6 +PKG_RELEASE:=7 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 @@ -50,8 +50,6 @@ endef define Package/ipv6-neigh/install $(INSTALL_DIR) $(1)/usr/bin/ $(INSTALL_BIN) $(PKG_INSTALL_DIR)/bin/ip-neigh $(1)/usr/bin/ipv6-neigh - $(INSTALL_DIR) $(1)/etc/init.d/ - $(INSTALL_BIN) ./files/etc/init.d/ipv6-neigh $(1)/etc/init.d/ipv6-neigh endef $(eval $(call RustBinPackage,ipv6-neigh)) diff --git a/ipv6-neigh/files/etc/init.d/ipv6-neigh b/ipv6-neigh/files/etc/init.d/ipv6-neigh deleted file mode 100644 index 33c95722..00000000 --- a/ipv6-neigh/files/etc/init.d/ipv6-neigh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh /etc/rc.common - -USE_PROCD=1 -START=19 -STOP=50 - -PROG=/usr/bin/ipv6-neigh - -start_service() { - procd_open_instance ipv6-neigh - # 默认: IPv4 仅私网;IPv6 允许公网(GUA) - procd_set_param command $PROG --private-subnet-v4 -d "[::1]:53" -z lan - procd_set_param stdout 1 - procd_set_param stderr 1 - procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" - procd_close_instance ipv6-neigh -} diff --git a/ipv6-neigh/src/db.rs b/ipv6-neigh/src/db.rs index 21bc545f..6ceebbc8 100644 --- a/ipv6-neigh/src/db.rs +++ b/ipv6-neigh/src/db.rs @@ -1,7 +1,7 @@ -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use hickory_proto::op::{Message, ResponseCode, update_message}; +use hickory_proto::op::{Message, Query, ResponseCode, update_message}; use hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; @@ -137,4 +137,95 @@ impl DnsUpdater { Ok(result) } + + /// Fetch all A and AAAA records in the zone via AXFR (RFC 5936). + /// + /// Returns a list of `(hostname_label, IpAddr)` pairs — only records whose owner + /// name is directly under the zone apex (e.g. `foo.lan.` → `"foo"`). + /// SOA, NS, and apex records are excluded. + /// + /// Requires `axfr_policy = "AllowAll"` (or `"AllowSigned"`) in the server config. + pub async fn axfr_records(&self) -> Result, Box> { + const AXFR_TIMEOUT: Duration = Duration::from_secs(10); + + let query = Query::query(self.zone.clone(), RecordType::AXFR); + let mut msg = Message::query(); + msg.add_query(query); + + let bytes = msg.to_vec()?; + let len = u16::try_from(bytes.len())?; + let zone = self.zone.clone(); + let addr = self.server_addr; + + let records = timeout(AXFR_TIMEOUT, async move { + let mut stream = TcpStream::connect(addr).await?; + stream.write_all(&len.to_be_bytes()).await?; + stream.write_all(&bytes).await?; + stream.flush().await?; + + let mut records: Vec<(String, IpAddr)> = Vec::new(); + let mut soa_count = 0u32; + + loop { + let resp_len = stream.read_u16().await? as usize; + if resp_len == 0 { + break; + } + let mut buf = vec![0u8; resp_len]; + stream.read_exact(&mut buf).await?; + let response = Message::from_vec(&buf)?; + + if response.metadata.response_code != ResponseCode::NoError { + return Err(format!("AXFR error: {:?}", response.metadata.response_code).into()); + } + + for record in &response.answers { + match &record.data { + RData::SOA(_) => { + soa_count += 1; + } + RData::A(a) => { + if let Some(host) = extract_hostname(&record.name, &zone) { + records.push((host, IpAddr::V4(a.0))); + } + } + RData::AAAA(aaaa) => { + if let Some(host) = extract_hostname(&record.name, &zone) { + records.push((host, IpAddr::V6(aaaa.0))); + } + } + _ => {} + } + } + + if soa_count >= 2 { + break; + } + } + + Ok::<_, Box>(records) + }) + .await + .map_err(|_| -> Box { + format!("AXFR timeout after {}s connecting to {}", AXFR_TIMEOUT.as_secs(), self.server_addr).into() + })??; + + Ok(records) + } +} + +/// Extract the single label that precedes the zone apex from a fully-qualified record name. +/// +/// e.g. `"foo.lan."` with zone `"lan."` → `Some("foo")`. +/// Returns `None` for the apex itself or for names not directly under the zone. +fn extract_hostname(name: &Name, zone: &Name) -> Option { + let n = name.to_ascii().to_lowercase(); + let z = zone.to_ascii().to_lowercase(); + let n = n.trim_end_matches('.'); + let z = z.trim_end_matches('.'); + if n == z { + return None; // apex record + } + let suffix = format!(".{z}"); + n.strip_suffix(&suffix).map(|s| s.to_string()) } diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index 5760d397..4d452974 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -13,6 +13,7 @@ use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, Neig use netlink_packet_route::route::RouteType; use rtnetlink::{Error, Handle, new_connection}; use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use std::os::unix::io::AsRawFd; use tokio::time::{self, Duration}; use clap::Parser; @@ -74,6 +75,12 @@ struct Neigh { mac: String, } +/// An IPv6 prefix (address + length) representing an active LAN prefix on the router interface. +struct LanPrefix { + addr: Ipv6Addr, + prefix_len: u8, +} + 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() @@ -107,6 +114,18 @@ fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool { 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) +} + async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) -> bool { let Some(hostname) = leases.get(&neigh.mac) else { debug!("no lease for mac {}, skipping DNS update", neigh.mac); @@ -158,11 +177,6 @@ fn should_skip_neigh(neigh: &Neigh) -> bool { should_skip_route_type(neigh.kind) } -/// Whether this NUD state indicates the neighbour is likely reachable (register in DNS). -fn is_reachable_state(state: NeighbourState) -> bool { - matches!(state, NeighbourState::Reachable | NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) -} - /// Whether this NUD state indicates the neighbour is definitely gone (remove from DNS). fn is_failed_state(state: NeighbourState) -> bool { matches!(state, NeighbourState::Failed) @@ -178,6 +192,9 @@ async fn main() -> Result<(), ()> { .parse() .expect("invalid log level (use: error, warn, info, debug, trace)"), ) + // Suppress spurious warnings from netlink_packet_route when the kernel + // provides more NLA data than the crate version expects (newer kernel). + .filter_module("netlink_packet_route", log::LevelFilter::Error) .format_timestamp_secs() .init(); @@ -193,6 +210,9 @@ async fn main() -> Result<(), ()> { let updater = db::DnsUpdater::new(args.dns_server, zone, signer); + // 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(); tokio::spawn(connection); @@ -218,21 +238,76 @@ async fn main() -> Result<(), ()> { let mut leases = op::get_lease().unwrap_or_default(); info!("loaded {} DHCP leases", leases.len()); - // State cache: tracks (mac, ip_string) -> last confirmed time - let mut registered: HashMap<(String, String), Instant> = HashMap::new(); + // State cache: tracks (mac, ip_string) -> (last confirmed time, ifindex) + let mut registered: HashMap<(String, String), (Instant, u32)> = HashMap::new(); - // Dump existing neighbours and register only reachable/stale ones + // Get active LAN prefixes to filter out neighbours from expired/old prefixes. + // If prefix detection fails we log a warning but continue without filtering. + let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, private_subnet_v6).await; + if active_prefixes.is_empty() { + warn!("no active IPv6 prefixes found on {}, neighbour prefix-filtering disabled", args.router_iface); + } else { + info!( + "active LAN IPv6 prefixes: {}", + active_prefixes + .iter() + .map(|p| format!("{}/{}", p.addr, p.prefix_len)) + .collect::>() + .join(", ") + ); + } + + // Dump existing neighbours: + // - Skip entries whose IP is not within any active LAN prefix (old-prefix residuals). + // - Register REACHABLE entries immediately. + // - Send a probe to STALE/DELAY/PROBE entries; the event loop will register them + // once the kernel confirms they are REACHABLE. + // - Ignore FAILED and other states. debug!("dumping neighbours"); if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4, private_subnet_v6).await { for neigh in &neighbours { debug!("{:?}", neigh); - if should_skip_neigh(neigh) || !is_reachable_state(neigh.state) { + if should_skip_neigh(neigh) { continue; } + // Drop IPv6 neighbours that are not within any active LAN prefix. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if !active_prefixes.is_empty() && !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)) { + debug!("init dump: skipping {} — not in any active LAN prefix", addr); + continue; + } + } let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if !registered.contains_key(&key) { - if process_new_neigh(neigh, &updater, &leases).await { - registered.insert(key, Instant::now()); + match neigh.state { + NeighbourState::Reachable => { + // Confirmed online — register in DNS immediately. + if !registered.contains_key(&key) { + if process_new_neigh(neigh, &updater, &leases).await { + registered.insert(key, (Instant::now(), neigh.ifindex)); + } + } + } + NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe => { + // Uncertain — probe to trigger NUD verification. If the device + // is online, the kernel will emit a REACHABLE NewNeighbour event + // which the event loop will pick up and register in DNS. + match &neigh.inet { + NeighbourAddress::Inet6(addr) => { + match send_icmpv6_echo(*addr, neigh.ifindex) { + Ok(()) => debug!("init dump: probing stale neighbour {}", addr), + Err(e) => debug!("init probe failed for {}: {}", addr, e), + } + } + NeighbourAddress::Inet(addr) => { + if let Err(e) = send_icmpv4_echo(*addr, neigh.ifindex) { + debug!("init probe failed for {}: {}", addr, e); + } + } + _ => {} + } + } + _ => { + // FAILED and others — skip. } } } @@ -297,20 +372,26 @@ async fn main() -> Result<(), ()> { debug!("Neighbour failed: {:?}", neigh); if !process_del_neigh(&neigh, &updater, &leases).await { // DNS delete failed, put back so we retry - registered.insert(key, Instant::now()); + registered.insert(key, (Instant::now(), neigh.ifindex)); } } - } else if is_reachable_state(neigh.state) { + } else if neigh.state == NeighbourState::Reachable { if registered.contains_key(&key) { // Already registered, just update timestamp - registered.insert(key, Instant::now()); + registered.insert(key, (Instant::now(), neigh.ifindex)); } else { // New reachable neighbour — register in DNS debug!("New neighbour: {:?}", neigh); if process_new_neigh(&neigh, &updater, &leases).await { - registered.insert(key, Instant::now()); + registered.insert(key, (Instant::now(), neigh.ifindex)); } } + } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { + // Uncertain state: keep existing registrations alive but do + // not create new DNS records without REACHABLE confirmation. + if registered.contains_key(&key) { + registered.insert(key, (Instant::now(), neigh.ifindex)); + } } } RouteNetlinkMessage::DelNeighbour(del_neigh) => { @@ -321,15 +402,11 @@ async fn main() -> Result<(), ()> { continue; } let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if registered.remove(&key).is_none() { - // Was not registered, skip - continue; - } + registered.remove(&key); debug!("Del neighbour: {:?}", neigh); - if !process_del_neigh(&neigh, &updater, &leases).await { - // DNS delete failed, put back so we retry delete next time - registered.insert(key, Instant::now()); - } + // Always attempt DNS deletion even if not in registered map, + // to clean up records that survived a program restart. + process_del_neigh(&neigh, &updater, &leases).await; } _ => {} } @@ -340,37 +417,176 @@ async fn main() -> Result<(), ()> { continue; } probe_registered_neighbours(®istered).await; + reconcile_dns( + &updater, + &mut registered, + &leases, + &active_prefixes, + private_subnet_v4, + private_subnet_v6, + ) + .await; } } } 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), (Instant, u32)>, + leases: &HashMap, + 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 = 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) --- + // Probe with ifindex=0 so the kernel routes via the default LAN route. + // If the host is alive the resulting REACHABLE event will re-populate `registered`. + // If gone, the record's TTL (60 s) will expire and it won't be renewed. + for (ip_str, hostname) in &dns_ips { + if registered_ips.contains(ip_str.as_str()) { + continue; + } + info!("reconcile: DNS orphan {} -> {}, probing", hostname, ip_str); + match ip_str.parse::() { + Ok(IpAddr::V6(addr)) => { + if let Err(e) = send_icmpv6_echo(addr, 0) { + debug!("reconcile: probe failed for {}: {}", addr, e); + } + } + Ok(IpAddr::V4(addr)) => { + let _ = send_icmpv4_echo(addr, 0); + } + _ => {} + } + } + + // --- Registered orphans (in registered but not in DNS) --- + // hickory-dns may have restarted and lost its journal; re-push the record. + for ((mac, ip_str), (last_confirmed, _)) in registered.iter_mut() { + if dns_ips.contains_key(ip_str.as_str()) { + continue; + } + let Some(hostname) = leases.get(mac) else { continue }; + debug!("reconcile: registered orphan {} -> {}, re-pushing", hostname, ip_str); + let result = match ip_str.parse::() { + 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); + *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), Instant>) { +async fn probe_registered_neighbours(registered: &HashMap<(String, String), (Instant, u32)>) { let now = Instant::now(); - for ((_, ip_str), last_confirmed) in registered.iter() { + for ((_, ip_str), (last_confirmed, ifindex)) in registered.iter() { // Only probe entries not confirmed recently (older than 30s) if now.duration_since(*last_confirmed) < Duration::from_secs(30) { continue; } if let Ok(addr) = ip_str.parse::() { - if let Err(e) = send_icmpv6_echo(addr) { + if let Err(e) = send_icmpv6_echo(addr, *ifindex) { debug!("probe failed for {}: {}", ip_str, e); } } else if let Ok(addr) = ip_str.parse::() { - if let Err(e) = send_icmpv4_echo(addr) { + if let Err(e) = send_icmpv4_echo(addr, *ifindex) { debug!("probe failed for {}: {}", ip_str, e); } } } } -fn send_icmpv6_echo(addr: Ipv6Addr) -> std::io::Result<()> { +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. + if ifindex != 0 { + unsafe { + let idx = ifindex as libc::c_int; + libc::setsockopt( + socket.as_raw_fd(), + libc::IPPROTO_IPV6, + libc::IPV6_UNICAST_IF, + &idx as *const libc::c_int as *const libc::c_void, + std::mem::size_of::() as libc::socklen_t, + ); + } + } // 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)); @@ -378,9 +594,22 @@ fn send_icmpv6_echo(addr: Ipv6Addr) -> std::io::Result<()> { Ok(()) } -fn send_icmpv4_echo(addr: Ipv4Addr) -> std::io::Result<()> { +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. + if ifindex != 0 { + unsafe { + let idx = ifindex as libc::c_int; + libc::setsockopt( + socket.as_raw_fd(), + libc::IPPROTO_IP, + libc::IP_UNICAST_IF, + &idx as *const libc::c_int as *const libc::c_void, + std::mem::size_of::() as libc::socklen_t, + ); + } + } // ICMPv4 Echo Request: type=8, code=0, checksum (simple for 8 bytes), id=0, seq=1 // Checksum for [08,00,00,00,00,00,00,01]: ~(0x0800 + 0x0001) = 0xf7fe let packet: [u8; 8] = [8, 0, 0xf7, 0xfe, 0, 0, 0, 1]; @@ -470,6 +699,36 @@ async fn dump_neighbours(handle: Handle, private_subnet_v4: bool, private_subnet Ok(vec) } +/// 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 { + 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; + 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, @@ -524,3 +783,21 @@ async fn register_router_addresses( } } } + +/// 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; + } + } + } +} diff --git a/ipv6-neigh/src/op.rs b/ipv6-neigh/src/op.rs index d51c249f..441f6be1 100644 --- a/ipv6-neigh/src/op.rs +++ b/ipv6-neigh/src/op.rs @@ -45,6 +45,32 @@ fn sanitize_hostname(raw: &str) -> String { collapsed.chars().take(63).collect() } +/// Extract a MAC address from a DHCPv6 DUID string. +/// Supports DUID-LLT (type 1) and DUID-LL (type 3) with any hardware type. +/// Returns the colon-separated MAC, e.g. "30:c5:99:7d:d5:ae". +fn mac_from_duid(duid: &str) -> Option { + // All chars must be hex digits + if !duid.chars().all(|c| c.is_ascii_hexdigit()) { + return None; + } + let duid_type = u16::from_str_radix(duid.get(0..4)?, 16).ok()?; + // DUID-LLT (1): type(4) + hw_type(4) + time(8) = 16 hex chars before MAC + // DUID-LL (3): type(4) + hw_type(4) = 8 hex chars before MAC + let mac_offset = match duid_type { + 1 => 16, + 3 => 8, + _ => return None, + }; + let mac_hex = duid.get(mac_offset..mac_offset + 12)?; + let mac = mac_hex + .as_bytes() + .chunks(2) + .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .collect::>() + .join(":"); + Some(mac) +} + // get mac to hostname mapping pub fn get_lease() -> Result, Box> { // dhcpv6 does not use mac address, so we only need to get ipv4 leases @@ -68,5 +94,26 @@ pub fn get_lease() -> Result, Box } } + // Supplement with IPv6 leases: extract MAC from DUID when available. + // IPv4 entries take precedence; only fill in missing MACs here. + if let Ok(v6leases) = call_ubus("dhcp", "ipv6leases") { + if let Some(devices) = v6leases["device"].as_object() { + for (_device, device_data) in devices { + let Some(leases) = device_data["leases"].as_array() else { continue }; + for lease in leases { + let Some(duid) = lease["duid"].as_str() else { continue }; + let Some(hostname) = lease["hostname"].as_str() else { continue }; + let hostname = sanitize_hostname(hostname); + if hostname.is_empty() { + continue; + } + let Some(mac) = mac_from_duid(duid) else { continue }; + // IPv4 mapping takes precedence + result.entry(mac).or_insert(hostname); + } + } + } + } + Ok(result) } diff --git a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js index a33449a6..f03bc616 100644 --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js @@ -32,22 +32,13 @@ application/json-seq: ␊ = \n | ^J | 0xa, ␞ = ␞ | ^^ | 0x1e return dm2.dv.extend({ load() { - const now = Math.floor(Date.now() / 1000); this.js_api = false; - - return Promise.all([ - dm2.docker_events({ query: { since: `0`, until: `${now}` } }), - dm2.js_api_ready.then(([ok, ]) => this.js_api = ok), - ]); + return Promise.resolve([]); }, - render([events, js_api_available]) { - if (events?.code !== 200) { - return E('div', {}, [ events?.body?.message ]); - } - - this.outputText = events?.body ? JSON.stringify(events?.body, null, 2) + '\n' : ''; - const event_list = events?.body || []; + render() { + this.outputText = ''; + const event_list = []; const view = this; const mainContainer = E('div', { 'class': 'cbi-map' }, [ @@ -98,7 +89,7 @@ return dm2.dv.extend({ E('input', { 'id': 'event-from-date', 'type': 'datetime-local', - 'value': '1970-01-01T00:00', + 'value': new Date(Date.now() - (60 * 60 * 1000)).toISOString().slice(0, 16), 'step': 60, 'style': 'width: 180px;', 'change': () => { view.renderEventsTable(event_list); } @@ -150,6 +141,16 @@ return dm2.dv.extend({ } }, _('Now')) ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, '\u00a0'), + E('div', { 'class': 'cbi-value-field' }, [ + E('button', { + 'type': 'button', + 'class': 'cbi-button cbi-button-action', + 'click': () => { view.renderEventsTable(event_list); } + }, _('Load Events')) + ]) ]) ]) ]); @@ -158,10 +159,18 @@ return dm2.dv.extend({ this.tableSection = E('div', { 'class': 'cbi-section', 'id': 'events-section' }); mainContainer.appendChild(this.tableSection); - this.renderEventsTable(event_list); + this.tableSection.appendChild(E('em', {}, _('Loading recent events…'))); mainContainer.appendChild(this.insertOutputFrame(E('div', {}), null)); + dm2.js_api_ready.then(([ok, ]) => { + view.js_api = ok; + }).catch(() => { + view.js_api = false; + }); + + window.setTimeout(() => view.renderEventsTable(event_list), 0); + return mainContainer; }, diff --git a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js index 8c25c179..e67e1abe 100644 --- a/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js +++ b/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js @@ -63,6 +63,154 @@ function getVolumesInUseByContainers(containers) { return inUse; } +function formatScalar(value) { + if (value == null || value === '') + return '-'; + + if (Array.isArray(value)) + return value.length ? value.join(', ') : '-'; + + if (typeof value === 'boolean') + return value ? 'true' : 'false'; + + return String(value); +} + +function formatKeyValueObject(obj) { + if (!obj || typeof obj !== 'object') + return formatScalar(obj); + + const pairs = []; + for (const [key, value] of Object.entries(obj)) { + if (value == null || value === '' || value === false) + continue; + + pairs.push(`${key}: ${formatScalar(value)}`); + } + + return pairs.length ? pairs.join('; ') : '-'; +} + +function formatPlugins(plugins) { + if (!plugins || typeof plugins !== 'object') + return '-'; + + const parts = []; + for (const [type, entries] of Object.entries(plugins)) { + if (!entries) + continue; + + if (Array.isArray(entries)) + parts.push(`${type}: ${entries.length ? entries.join(', ') : '-'}`); + else + parts.push(`${type}: ${formatScalar(entries)}`); + } + + return parts.length ? parts.join('; ') : '-'; +} + +function formatRegistryConfig(registryConfig) { + if (!registryConfig || typeof registryConfig !== 'object') + return '-'; + + const parts = []; + if (Array.isArray(registryConfig.Mirrors) && registryConfig.Mirrors.length) + parts.push(`Mirrors: ${registryConfig.Mirrors.join(', ')}`); + + if (Array.isArray(registryConfig.InsecureRegistryCIDRs) && registryConfig.InsecureRegistryCIDRs.length) + parts.push(`Insecure CIDRs: ${registryConfig.InsecureRegistryCIDRs.join(', ')}`); + + return parts.length ? parts.join('; ') : '-'; +} + +function formatRuntimes(runtimes) { + if (!runtimes || typeof runtimes !== 'object') + return '-'; + + const parts = []; + for (const [name, runtime] of Object.entries(runtimes)) { + const path = runtime?.path ? ` (${runtime.path})` : ''; + parts.push(`${name}${path}`); + } + + return parts.length ? parts.join(', ') : '-'; +} + +function formatSwarm(swarm) { + if (!swarm || typeof swarm !== 'object') + return '-'; + + const parts = []; + if (swarm.LocalNodeState) + parts.push(`State: ${swarm.LocalNodeState}`); + if (swarm.NodeID) + parts.push(`NodeID: ${swarm.NodeID}`); + if (swarm.NodeAddr) + parts.push(`NodeAddr: ${swarm.NodeAddr}`); + if (swarm.ControlAvailable != null) + parts.push(`Control: ${swarm.ControlAvailable ? 'yes' : 'no'}`); + + return parts.length ? parts.join('; ') : '-'; +} + +function formatContainerd(containerd) { + if (!containerd || typeof containerd !== 'object') + return '-'; + + const parts = []; + if (containerd.Address) + parts.push(`Address: ${containerd.Address}`); + if (containerd.Namespaces && typeof containerd.Namespaces === 'object') + parts.push(`Namespaces: ${formatKeyValueObject(containerd.Namespaces)}`); + + return parts.length ? parts.join('; ') : '-'; +} + +function formatInfoBody(body) { + const rows = []; + for (const [key, value] of Object.entries(body || {})) { + if (!value) + continue; + + let formatted = null; + switch (key) { + case 'Plugins': + formatted = formatPlugins(value); + break; + case 'RegistryConfig': + formatted = formatRegistryConfig(value); + break; + case 'Runtimes': + formatted = formatRuntimes(value); + break; + case 'Swarm': + formatted = formatSwarm(value); + break; + case 'Containerd': + formatted = formatContainerd(value); + break; + case 'Warnings': + case 'Labels': + case 'SecurityOptions': + case 'CDISpecDirs': + formatted = formatScalar(value); + break; + case 'DriverStatus': + formatted = Array.isArray(value) ? value.map(v => Array.isArray(v) ? `${v[0]}: ${v[1]}` : formatScalar(v)).join('; ') : formatScalar(value); + break; + default: + if (typeof value === 'object') + formatted = formatKeyValueObject(value); + else + formatted = formatScalar(value); + } + + rows.push({ entry: key, value: formatted }); + } + + return rows; +} + return dm2.dv.extend({ load() { @@ -115,7 +263,7 @@ return dm2.dv.extend({ this.parseHeaders(version_response.headers, version_headers); this.parseBody(version_response.body, version_body); - this.parseBody(info_response.body, info_body); + info_body.push(...formatInfoBody(info_response.body)); // this.parseBody(df_response.body, df_body); const view = this; const info = info_response.body; diff --git a/luci-theme-design/Makefile b/luci-theme-design/Makefile index f469c682..ca64338b 100644 --- a/luci-theme-design/Makefile +++ b/luci-theme-design/Makefile @@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme LUCI_DEPENDS:= PKG_LICENSE:=Apache-2.0 PKG_VERSION:=7.1 -PKG_RELEASE:=5 +PKG_RELEASE:=6 LUCI_MINIFY_CSS:= CONFIG_LUCI_CSSTIDY:= diff --git a/luci-theme-design/htdocs/luci-static/design/css/style.css b/luci-theme-design/htdocs/luci-static/design/css/style.css index c9edb931..06a23df4 100644 --- a/luci-theme-design/htdocs/luci-static/design/css/style.css +++ b/luci-theme-design/htdocs/luci-static/design/css/style.css @@ -4735,6 +4735,45 @@ body.modal-overlay-active #modal_overlay .modal.cbi-modal .cbi-value-field.cbi-d margin-bottom: 1rem; } +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table { + width: 100% !important; + table-layout: fixed !important; +} + +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .tr > .td, +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .tr > .th { + display: table-cell !important; + vertical-align: top !important; +} + +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .tr > .td:first-child, +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .tr > .th:first-child { + width: 14rem !important; +} + +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .td[data-title="Entry"], +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .th[data-title="Entry"] { + width: 14rem !important; +} + +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .td[data-title="Value"], +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .td[data-title="值"] { + text-align: center !important; + white-space: normal !important; +} + +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .td[data-title="Value"] output, +body.node-admin-services-dockerman-overview[data-page="admin-services-dockerman"] .table.cbi-section-table .td[data-title="值"] output { + display: block !important; + width: min(100%, 72rem); + max-width: 100%; + margin: 0 auto; + white-space: normal !important; + overflow-wrap: anywhere !important; + word-break: break-word !important; + text-align: center !important; +} + [data-page="admin-network-network"] .ifacebox { margin: 0.5rem; border-radius: 8px; diff --git a/luci-theme-design/htdocs/luci-static/resources/menu-design.js b/luci-theme-design/htdocs/luci-static/resources/menu-design.js index 43ebef7c..f04ff018 100644 --- a/luci-theme-design/htdocs/luci-static/resources/menu-design.js +++ b/luci-theme-design/htdocs/luci-static/resources/menu-design.js @@ -77,12 +77,32 @@ return baseclass.extend({ window.location.replace(url.toString()); }, + getFileListStamp: function(path, suffix) { + return L.resolveDefault(fs.list(path), []).then(function(entries) { + return entries.filter(function(entry) { + return entry && entry.type === 'file' && + (suffix == null || entry.name.endsWith(suffix)); + }).map(function(entry) { + return [ entry.name, entry.mtime || 0, entry.size || 0 ].join(':'); + }).sort().join('|'); + }); + }, + getConditionalMenuStamp: function() { var googleFuMode = !!document.querySelector('.navbar a[href*="/admin/services/openclash"]'); - return Promise.resolve(JSON.stringify({ - google_fu_mode: googleFuMode - })); + return Promise.all([ + this.getFileListStamp('/usr/share/luci/menu.d', '.json'), + this.getFileListStamp('/usr/share/rpcd/acl.d', '.json'), + L.resolveDefault(fs.stat('/etc/config/wireless'), null) + ]).then(function(stamps) { + return JSON.stringify({ + google_fu_mode: googleFuMode, + menu_definitions: stamps[0], + acl_definitions: stamps[1], + wireless_config: stamps[2] ? [ stamps[2].mtime || 0, stamps[2].size || 0 ].join(':') : 'absent' + }); + }); }, flushBackendMenuCache: function() { diff --git a/luci-theme-design/ucode/controller/design_theme.uc b/luci-theme-design/ucode/controller/design_theme.uc index 14e895e6..1fdbb9be 100644 --- a/luci-theme-design/ucode/controller/design_theme.uc +++ b/luci-theme-design/ucode/controller/design_theme.uc @@ -4,10 +4,13 @@ import { glob, unlink } from 'fs'; return { menu_flush: function() { - for (let path in glob('/tmp/luci-indexcache', '/tmp/luci-indexcache.*')) + for (let path in glob( + '/tmp/luci-indexcache', '/tmp/luci-indexcache.*', + '/var/luci-indexcache', '/var/luci-indexcache.*' + )) unlink(path); - system('rm -rf /tmp/luci-modulecache 2>/dev/null'); + system('rm -rf /tmp/luci-modulecache /var/luci-modulecache 2>/dev/null'); http.prepare_content('application/json'); http.write_json({ flushed: true }); diff --git a/mihomo-alpha/Makefile b/mihomo-alpha/Makefile index 2ed02cb5..795baff8 100644 --- a/mihomo-alpha/Makefile +++ b/mihomo-alpha/Makefile @@ -1,14 +1,14 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mihomo-alpha -PKG_VERSION:=2026.04.20 -PKG_RELEASE:=5 +PKG_VERSION:=2026.05.08 +PKG_RELEASE:=6 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git -PKG_SOURCE_VERSION:=c59c99a051c5c4fb3c2a7454d7c00fea9a8ee4bb +PKG_SOURCE_VERSION:=35d5d4e44d7a7b8352617b1fec7faf0ba26efc8a PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL3.0+ @@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_PARALLEL:=1 PKG_BUILD_FLAGS:=no-mips16 -PKG_BUILD_VERSION:=alpha-c59c99a0 +PKG_BUILD_VERSION:=alpha-35d5d4e4 PKG_BUILD_TIME:=$(shell date -u -Iseconds) GO_PKG:=github.com/metacubex/mihomo diff --git a/ooniprobe/Makefile b/ooniprobe/Makefile index 765eb851..787c7c7b 100644 --- a/ooniprobe/Makefile +++ b/ooniprobe/Makefile @@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ooniprobe -PKG_VERSION:=3.29.0 -PKG_RELEASE:=1 +PKG_VERSION:=3.29.1 +PKG_RELEASE:=2 PKG_SOURCE:=probe-cli-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/ooni/probe-cli/tar.gz/v$(PKG_VERSION)? diff --git a/qminfo/Makefile b/qminfo/Makefile index 50c8ac77..d16ee3e9 100644 --- a/qminfo/Makefile +++ b/qminfo/Makefile @@ -8,7 +8,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=qminfo -PKG_RELEASE:=1 +PKG_VERSION=0.0.1 +PKG_RELEASE:=2 include $(INCLUDE_DIR)/package.mk diff --git a/qminfo/src/qminfo.c b/qminfo/src/qminfo.c index d33e48bd..b7ed0009 100644 --- a/qminfo/src/qminfo.c +++ b/qminfo/src/qminfo.c @@ -348,17 +348,17 @@ static void emit_results(void) printf(" CID : %llu\n",(unsigned long long)info.cid); printf(" eNB ID : %u\n", info.enb_id); printf(" Sector : %u\n", info.cell_sector); - if (info.has_arfcn) printf(" ARFCN : %u\n", info.arfcn); + if (info.has_arfcn) printf(" RF Chan. : %u\n", info.arfcn); if (info.has_pci) printf(" PCI : %u\n", info.pci); if (dist_km >= 0) printf(" Distance : ~%.2f km (TA=%u)\n", dist_km, info.ta); if (info.has_bw_dl && strcmp(info.mode, "LTE") == 0) printf(" BW DL : %.1f MHz\n", bw_index_to_khz(info.bw_dl) / 1000.0f); printf("-------------------------------------------------\n"); + if (info.has_csq) printf(" Strength : %d%% (CSQ %d)\n", csq_pct, info.csq); printf(" RSSI : %d dBm\n", info.rssi); if (info.has_rsrp) printf(" RSRP : %d dBm\n", (int)roundf(info.rsrp)); if (info.has_rsrq) printf(" RSRQ : %d dB\n", (int)roundf(info.rsrq)); if (info.has_sinr) printf(" %s : %d dB\n", siname, (int)roundf(info.sinr / 10.0f)); - if (info.has_csq) printf(" CSQ : %d (%d%%)\n", info.csq, csq_pct); if (info.has_ca) { printf(" LTE-A SCC : %d — %s\n", info.lte_ca, info.scc_bands); printf(" BW CA : %.1f MHz\n", info.bw_ca_total / 1000.0f); @@ -380,7 +380,7 @@ static void emit_results(void) const char *csq_col = ""; if (info.has_csq && csq_pct >= 0) - csq_col = (info.csq > 20) ? "green" : (info.csq > 10) ? "yellow" : "red"; + csq_col = (info.csq > 20) ? "green" : (info.csq > 10) ? "orange" : "red"; char csq_per_str[16] = ""; if (csq_pct >= 0) snprintf(csq_per_str, sizeof(csq_per_str), "%d", csq_pct); @@ -967,10 +967,10 @@ static void on_model(QmiClientDms *client, GAsyncResult *res, gpointer ud) } info.has_model = TRUE; } else if (usb_product[0]) { - strncpy(info.model, usb_product, sizeof(info.model) - 1); + g_strlcpy(info.model, usb_product, sizeof(info.model)); info.has_model = TRUE; } else if (usb_manufacturer[0]) { - strncpy(info.model, usb_manufacturer, sizeof(info.model) - 1); + g_strlcpy(info.model, usb_manufacturer, sizeof(info.model)); info.has_model = TRUE; }