mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-06 03:14:12 +08:00
🐤 Sync 2026-05-08 20:31:49
This commit is contained in:
1 parent
dc55b8fdfe
commit
e2f4c3d0b5
22 files changed
+738
-92
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=geoview
|
PKG_NAME:=geoview
|
||||||
PKG_VERSION:=0.2.5
|
PKG_VERSION:=0.2.6
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=hickory-dns
|
PKG_NAME:=hickory-dns
|
||||||
PKG_VERSION:=0.26.1
|
PKG_VERSION:=0.26.1
|
||||||
PKG_RELEASE:=21
|
PKG_RELEASE:=22
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ file = "/etc/hickory-dns/0.zone"
|
|||||||
[[zones]]
|
[[zones]]
|
||||||
zone = "lan"
|
zone = "lan"
|
||||||
zone_type = "Primary"
|
zone_type = "Primary"
|
||||||
|
axfr_policy = "AllowAll"
|
||||||
|
|
||||||
[zones.stores]
|
[zones.stores]
|
||||||
type = "sqlite"
|
type = "sqlite"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@ STOP=51
|
|||||||
PROG=/usr/bin/hickory-dns
|
PROG=/usr/bin/hickory-dns
|
||||||
CONF=/etc/hickory-dns/forwarder.toml
|
CONF=/etc/hickory-dns/forwarder.toml
|
||||||
TSIG_KEY=/etc/hickory-dns/update.key
|
TSIG_KEY=/etc/hickory-dns/update.key
|
||||||
|
NEIGH_PROG=/usr/bin/ipv6-neigh
|
||||||
|
|
||||||
generate_tsig_key() {
|
generate_tsig_key() {
|
||||||
[ -f "$TSIG_KEY" ] && return 0
|
[ -f "$TSIG_KEY" ] && return 0
|
||||||
@@ -27,7 +28,11 @@ start_service() {
|
|||||||
procd_set_param stderr 1
|
procd_set_param stderr 1
|
||||||
procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}"
|
procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}"
|
||||||
procd_close_instance hickory-dns
|
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.
|
procd_open_instance ipv6-neigh
|
||||||
/etc/init.d/ipv6-neigh restart
|
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
|
||||||
}
|
}
|
||||||
Generated
+1
@@ -558,6 +558,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"hickory-proto",
|
"hickory-proto",
|
||||||
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"netlink-packet-core",
|
"netlink-packet-core",
|
||||||
"netlink-packet-route",
|
"netlink-packet-route",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ serde_json = "1.0.108"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
socket2 = "0.5"
|
socket2 = "0.5"
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
+1
-3
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=ipv6-neigh
|
PKG_NAME:=ipv6-neigh
|
||||||
PKG_VERSION:=0.1.0
|
PKG_VERSION:=0.1.0
|
||||||
PKG_RELEASE:=6
|
PKG_RELEASE:=7
|
||||||
|
|
||||||
PKG_BUILD_DEPENDS:=rust/host
|
PKG_BUILD_DEPENDS:=rust/host
|
||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
@@ -50,8 +50,6 @@ endef
|
|||||||
define Package/ipv6-neigh/install
|
define Package/ipv6-neigh/install
|
||||||
$(INSTALL_DIR) $(1)/usr/bin/
|
$(INSTALL_DIR) $(1)/usr/bin/
|
||||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/bin/ip-neigh $(1)/usr/bin/ipv6-neigh
|
$(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
|
endef
|
||||||
|
|
||||||
$(eval $(call RustBinPackage,ipv6-neigh))
|
$(eval $(call RustBinPackage,ipv6-neigh))
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
+93
-2
@@ -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 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 hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
@@ -137,4 +137,95 @@ impl DnsUpdater {
|
|||||||
|
|
||||||
Ok(result)
|
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<Vec<(String, IpAddr)>, Box<dyn std::error::Error>> {
|
||||||
|
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<dyn std::error::Error>>(records)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| -> Box<dyn std::error::Error> {
|
||||||
|
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<String> {
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
+307
-30
@@ -13,6 +13,7 @@ use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, Neig
|
|||||||
use netlink_packet_route::route::RouteType;
|
use netlink_packet_route::route::RouteType;
|
||||||
use rtnetlink::{Error, Handle, new_connection};
|
use rtnetlink::{Error, Handle, new_connection};
|
||||||
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
use tokio::time::{self, Duration};
|
use tokio::time::{self, Duration};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -74,6 +75,12 @@ struct Neigh {
|
|||||||
mac: String,
|
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 {
|
fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool {
|
||||||
// Check if address is ULA (fc00::/7)
|
// Check if address is ULA (fc00::/7)
|
||||||
// Note: link-local is always filtered earlier by is_link_local_ipv6()
|
// 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
|
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<String, String>) -> bool {
|
async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap<String, String>) -> bool {
|
||||||
let Some(hostname) = leases.get(&neigh.mac) else {
|
let Some(hostname) = leases.get(&neigh.mac) else {
|
||||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
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)
|
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).
|
/// Whether this NUD state indicates the neighbour is definitely gone (remove from DNS).
|
||||||
fn is_failed_state(state: NeighbourState) -> bool {
|
fn is_failed_state(state: NeighbourState) -> bool {
|
||||||
matches!(state, NeighbourState::Failed)
|
matches!(state, NeighbourState::Failed)
|
||||||
@@ -178,6 +192,9 @@ async fn main() -> Result<(), ()> {
|
|||||||
.parse()
|
.parse()
|
||||||
.expect("invalid log level (use: error, warn, info, debug, trace)"),
|
.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()
|
.format_timestamp_secs()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
@@ -193,6 +210,9 @@ async fn main() -> Result<(), ()> {
|
|||||||
|
|
||||||
let updater = db::DnsUpdater::new(args.dns_server, zone, signer);
|
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();
|
let (connection, handle, _) = new_connection().unwrap();
|
||||||
tokio::spawn(connection);
|
tokio::spawn(connection);
|
||||||
|
|
||||||
@@ -218,21 +238,76 @@ async fn main() -> Result<(), ()> {
|
|||||||
let mut leases = op::get_lease().unwrap_or_default();
|
let mut leases = op::get_lease().unwrap_or_default();
|
||||||
info!("loaded {} DHCP leases", leases.len());
|
info!("loaded {} DHCP leases", leases.len());
|
||||||
|
|
||||||
// State cache: tracks (mac, ip_string) -> last confirmed time
|
// State cache: tracks (mac, ip_string) -> (last confirmed time, ifindex)
|
||||||
let mut registered: HashMap<(String, String), Instant> = HashMap::new();
|
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::<Vec<_>>()
|
||||||
|
.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");
|
debug!("dumping neighbours");
|
||||||
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4, private_subnet_v6).await {
|
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4, private_subnet_v6).await {
|
||||||
for neigh in &neighbours {
|
for neigh in &neighbours {
|
||||||
debug!("{:?}", neigh);
|
debug!("{:?}", neigh);
|
||||||
if should_skip_neigh(neigh) || !is_reachable_state(neigh.state) {
|
if should_skip_neigh(neigh) {
|
||||||
continue;
|
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));
|
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
||||||
if !registered.contains_key(&key) {
|
match neigh.state {
|
||||||
if process_new_neigh(neigh, &updater, &leases).await {
|
NeighbourState::Reachable => {
|
||||||
registered.insert(key, Instant::now());
|
// 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);
|
debug!("Neighbour failed: {:?}", neigh);
|
||||||
if !process_del_neigh(&neigh, &updater, &leases).await {
|
if !process_del_neigh(&neigh, &updater, &leases).await {
|
||||||
// DNS delete failed, put back so we retry
|
// 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) {
|
if registered.contains_key(&key) {
|
||||||
// Already registered, just update timestamp
|
// Already registered, just update timestamp
|
||||||
registered.insert(key, Instant::now());
|
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||||
} else {
|
} else {
|
||||||
// New reachable neighbour — register in DNS
|
// New reachable neighbour — register in DNS
|
||||||
debug!("New neighbour: {:?}", neigh);
|
debug!("New neighbour: {:?}", neigh);
|
||||||
if process_new_neigh(&neigh, &updater, &leases).await {
|
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) => {
|
RouteNetlinkMessage::DelNeighbour(del_neigh) => {
|
||||||
@@ -321,15 +402,11 @@ async fn main() -> Result<(), ()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
||||||
if registered.remove(&key).is_none() {
|
registered.remove(&key);
|
||||||
// Was not registered, skip
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
debug!("Del neighbour: {:?}", neigh);
|
debug!("Del neighbour: {:?}", neigh);
|
||||||
if !process_del_neigh(&neigh, &updater, &leases).await {
|
// Always attempt DNS deletion even if not in registered map,
|
||||||
// DNS delete failed, put back so we retry delete next time
|
// to clean up records that survived a program restart.
|
||||||
registered.insert(key, Instant::now());
|
process_del_neigh(&neigh, &updater, &leases).await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -340,37 +417,176 @@ async fn main() -> Result<(), ()> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
probe_registered_neighbours(®istered).await;
|
probe_registered_neighbours(®istered).await;
|
||||||
|
reconcile_dns(
|
||||||
|
&updater,
|
||||||
|
&mut registered,
|
||||||
|
&leases,
|
||||||
|
&active_prefixes,
|
||||||
|
private_subnet_v4,
|
||||||
|
private_subnet_v6,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconcile the in-memory `registered` map against the live DNS zone obtained via AXFR.
|
||||||
|
///
|
||||||
|
/// Two corrections are made on each call:
|
||||||
|
/// 1. **DNS orphans** – records present in DNS but absent from `registered`.
|
||||||
|
/// These are either stale leftovers from a previous run or records from a prefix that
|
||||||
|
/// is no longer active. Records that fail prefix/subnet filtering are deleted from DNS
|
||||||
|
/// immediately; records that pass filtering are probed with ICMP so the kernel NUD
|
||||||
|
/// state machine can confirm reachability and re-populate `registered` via the event loop.
|
||||||
|
/// 2. **Registered orphans** – entries in `registered` that are missing from DNS (e.g.
|
||||||
|
/// because hickory-dns restarted and lost its in-memory state). These are re-pushed
|
||||||
|
/// via DNS UPDATE so the zone stays consistent.
|
||||||
|
async fn reconcile_dns(
|
||||||
|
updater: &db::DnsUpdater,
|
||||||
|
registered: &mut HashMap<(String, String), (Instant, u32)>,
|
||||||
|
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) ---
|
||||||
|
// 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::<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)) => {
|
||||||
|
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::<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);
|
||||||
|
*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.
|
/// Send ICMPv6 Echo Request / ICMPv4 Echo Request to all registered neighbours.
|
||||||
/// This forces the kernel NUD state machine to verify reachability, generating
|
/// This forces the kernel NUD state machine to verify reachability, generating
|
||||||
/// NewNeighbour events with the resulting state (Reachable or Failed).
|
/// 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();
|
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)
|
// Only probe entries not confirmed recently (older than 30s)
|
||||||
if now.duration_since(*last_confirmed) < Duration::from_secs(30) {
|
if now.duration_since(*last_confirmed) < Duration::from_secs(30) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(addr) = ip_str.parse::<Ipv6Addr>() {
|
if let Ok(addr) = ip_str.parse::<Ipv6Addr>() {
|
||||||
if let Err(e) = send_icmpv6_echo(addr) {
|
if let Err(e) = send_icmpv6_echo(addr, *ifindex) {
|
||||||
debug!("probe failed for {}: {}", ip_str, e);
|
debug!("probe failed for {}: {}", ip_str, e);
|
||||||
}
|
}
|
||||||
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
|
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
|
||||||
if let Err(e) = send_icmpv4_echo(addr) {
|
if let Err(e) = send_icmpv4_echo(addr, *ifindex) {
|
||||||
debug!("probe failed for {}: {}", ip_str, e);
|
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))?;
|
let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::ICMPV6))?;
|
||||||
socket.set_nonblocking(true)?;
|
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::<libc::c_int>() as libc::socklen_t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1
|
// 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 packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1];
|
||||||
let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0));
|
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(())
|
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))?;
|
let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?;
|
||||||
socket.set_nonblocking(true)?;
|
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::<libc::c_int>() as libc::socklen_t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ICMPv4 Echo Request: type=8, code=0, checksum (simple for 8 bytes), id=0, seq=1
|
// 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
|
// 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];
|
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)
|
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<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;
|
||||||
|
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.
|
/// Enumerate addresses on `iface`, register A/AAAA records for the router itself.
|
||||||
async fn register_router_addresses(
|
async fn register_router_addresses(
|
||||||
handle: Handle,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,32 @@ fn sanitize_hostname(raw: &str) -> String {
|
|||||||
collapsed.chars().take(63).collect()
|
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<String> {
|
||||||
|
// 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::<Vec<&str>>()
|
||||||
|
.join(":");
|
||||||
|
Some(mac)
|
||||||
|
}
|
||||||
|
|
||||||
// get mac to hostname mapping
|
// get mac to hostname mapping
|
||||||
pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
|
pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
|
||||||
// dhcpv6 does not use mac address, so we only need to get ipv4 leases
|
// dhcpv6 does not use mac address, so we only need to get ipv4 leases
|
||||||
@@ -68,5 +94,26 @@ pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@@ -32,22 +32,13 @@ application/json-seq: ␊ = \n | ^J | 0xa, ␞ = ␞ | ^^ | 0x1e
|
|||||||
|
|
||||||
return dm2.dv.extend({
|
return dm2.dv.extend({
|
||||||
load() {
|
load() {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
this.js_api = false;
|
this.js_api = false;
|
||||||
|
return Promise.resolve([]);
|
||||||
return Promise.all([
|
|
||||||
dm2.docker_events({ query: { since: `0`, until: `${now}` } }),
|
|
||||||
dm2.js_api_ready.then(([ok, ]) => this.js_api = ok),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render([events, js_api_available]) {
|
render() {
|
||||||
if (events?.code !== 200) {
|
this.outputText = '';
|
||||||
return E('div', {}, [ events?.body?.message ]);
|
const event_list = [];
|
||||||
}
|
|
||||||
|
|
||||||
this.outputText = events?.body ? JSON.stringify(events?.body, null, 2) + '\n' : '';
|
|
||||||
const event_list = events?.body || [];
|
|
||||||
const view = this;
|
const view = this;
|
||||||
|
|
||||||
const mainContainer = E('div', { 'class': 'cbi-map' }, [
|
const mainContainer = E('div', { 'class': 'cbi-map' }, [
|
||||||
@@ -98,7 +89,7 @@ return dm2.dv.extend({
|
|||||||
E('input', {
|
E('input', {
|
||||||
'id': 'event-from-date',
|
'id': 'event-from-date',
|
||||||
'type': 'datetime-local',
|
'type': 'datetime-local',
|
||||||
'value': '1970-01-01T00:00',
|
'value': new Date(Date.now() - (60 * 60 * 1000)).toISOString().slice(0, 16),
|
||||||
'step': 60,
|
'step': 60,
|
||||||
'style': 'width: 180px;',
|
'style': 'width: 180px;',
|
||||||
'change': () => { view.renderEventsTable(event_list); }
|
'change': () => { view.renderEventsTable(event_list); }
|
||||||
@@ -150,6 +141,16 @@ return dm2.dv.extend({
|
|||||||
}
|
}
|
||||||
}, _('Now'))
|
}, _('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' });
|
this.tableSection = E('div', { 'class': 'cbi-section', 'id': 'events-section' });
|
||||||
mainContainer.appendChild(this.tableSection);
|
mainContainer.appendChild(this.tableSection);
|
||||||
|
|
||||||
this.renderEventsTable(event_list);
|
this.tableSection.appendChild(E('em', {}, _('Loading recent events…')));
|
||||||
|
|
||||||
mainContainer.appendChild(this.insertOutputFrame(E('div', {}), null));
|
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;
|
return mainContainer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,154 @@ function getVolumesInUseByContainers(containers) {
|
|||||||
return inUse;
|
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({
|
return dm2.dv.extend({
|
||||||
load() {
|
load() {
|
||||||
@@ -115,7 +263,7 @@ return dm2.dv.extend({
|
|||||||
|
|
||||||
this.parseHeaders(version_response.headers, version_headers);
|
this.parseHeaders(version_response.headers, version_headers);
|
||||||
this.parseBody(version_response.body, version_body);
|
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);
|
// this.parseBody(df_response.body, df_body);
|
||||||
const view = this;
|
const view = this;
|
||||||
const info = info_response.body;
|
const info = info_response.body;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ LUCI_TITLE:=Design Theme
|
|||||||
LUCI_DEPENDS:=
|
LUCI_DEPENDS:=
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_VERSION:=7.1
|
PKG_VERSION:=7.1
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
|
|
||||||
LUCI_MINIFY_CSS:=
|
LUCI_MINIFY_CSS:=
|
||||||
CONFIG_LUCI_CSSTIDY:=
|
CONFIG_LUCI_CSSTIDY:=
|
||||||
|
|||||||
@@ -4735,6 +4735,45 @@ body.modal-overlay-active #modal_overlay .modal.cbi-modal .cbi-value-field.cbi-d
|
|||||||
margin-bottom: 1rem;
|
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 {
|
[data-page="admin-network-network"] .ifacebox {
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -77,12 +77,32 @@ return baseclass.extend({
|
|||||||
window.location.replace(url.toString());
|
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() {
|
getConditionalMenuStamp: function() {
|
||||||
var googleFuMode = !!document.querySelector('.navbar a[href*="/admin/services/openclash"]');
|
var googleFuMode = !!document.querySelector('.navbar a[href*="/admin/services/openclash"]');
|
||||||
|
|
||||||
return Promise.resolve(JSON.stringify({
|
return Promise.all([
|
||||||
google_fu_mode: googleFuMode
|
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() {
|
flushBackendMenuCache: function() {
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import { glob, unlink } from 'fs';
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
menu_flush: function() {
|
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);
|
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.prepare_content('application/json');
|
||||||
http.write_json({ flushed: true });
|
http.write_json({ flushed: true });
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=mihomo-alpha
|
PKG_NAME:=mihomo-alpha
|
||||||
PKG_VERSION:=2026.04.20
|
PKG_VERSION:=2026.05.08
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||||
PKG_SOURCE_VERSION:=c59c99a051c5c4fb3c2a7454d7c00fea9a8ee4bb
|
PKG_SOURCE_VERSION:=35d5d4e44d7a7b8352617b1fec7faf0ba26efc8a
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL3.0+
|
PKG_LICENSE:=GPL3.0+
|
||||||
@@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
|||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
PKG_BUILD_FLAGS:=no-mips16
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
PKG_BUILD_VERSION:=alpha-c59c99a0
|
PKG_BUILD_VERSION:=alpha-35d5d4e4
|
||||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||||
|
|
||||||
GO_PKG:=github.com/metacubex/mihomo
|
GO_PKG:=github.com/metacubex/mihomo
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=ooniprobe
|
PKG_NAME:=ooniprobe
|
||||||
PKG_VERSION:=3.29.0
|
PKG_VERSION:=3.29.1
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
PKG_SOURCE:=probe-cli-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=probe-cli-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/ooni/probe-cli/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/ooni/probe-cli/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
+2
-1
@@ -8,7 +8,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=qminfo
|
PKG_NAME:=qminfo
|
||||||
PKG_RELEASE:=1
|
PKG_VERSION=0.0.1
|
||||||
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
include $(INCLUDE_DIR)/package.mk
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -348,17 +348,17 @@ static void emit_results(void)
|
|||||||
printf(" CID : %llu\n",(unsigned long long)info.cid);
|
printf(" CID : %llu\n",(unsigned long long)info.cid);
|
||||||
printf(" eNB ID : %u\n", info.enb_id);
|
printf(" eNB ID : %u\n", info.enb_id);
|
||||||
printf(" Sector : %u\n", info.cell_sector);
|
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 (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 (dist_km >= 0) printf(" Distance : ~%.2f km (TA=%u)\n", dist_km, info.ta);
|
||||||
if (info.has_bw_dl && strcmp(info.mode, "LTE") == 0)
|
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(" BW DL : %.1f MHz\n", bw_index_to_khz(info.bw_dl) / 1000.0f);
|
||||||
printf("-------------------------------------------------\n");
|
printf("-------------------------------------------------\n");
|
||||||
|
if (info.has_csq) printf(" Strength : %d%% (CSQ %d)\n", csq_pct, info.csq);
|
||||||
printf(" RSSI : %d dBm\n", info.rssi);
|
printf(" RSSI : %d dBm\n", info.rssi);
|
||||||
if (info.has_rsrp) printf(" RSRP : %d dBm\n", (int)roundf(info.rsrp));
|
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_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_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) {
|
if (info.has_ca) {
|
||||||
printf(" LTE-A SCC : %d — %s\n", info.lte_ca, info.scc_bands);
|
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);
|
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 = "";
|
const char *csq_col = "";
|
||||||
if (info.has_csq && csq_pct >= 0)
|
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] = "";
|
char csq_per_str[16] = "";
|
||||||
if (csq_pct >= 0) snprintf(csq_per_str, sizeof(csq_per_str), "%d", csq_pct);
|
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;
|
info.has_model = TRUE;
|
||||||
} else if (usb_product[0]) {
|
} 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;
|
info.has_model = TRUE;
|
||||||
} else if (usb_manufacturer[0]) {
|
} 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;
|
info.has_model = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user