diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index a337e7e1..fb25e308 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:=27 +PKG_RELEASE:=28 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/0.0.10.zone b/hickory-dns/files/etc/hickory-dns/0.0.10.zone new file mode 100644 index 00000000..9b6b86ee --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/0.0.10.zone @@ -0,0 +1,8 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 1 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/3.168.192.zone b/hickory-dns/files/etc/hickory-dns/3.168.192.zone new file mode 100644 index 00000000..9b6b86ee --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/3.168.192.zone @@ -0,0 +1,8 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 1 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone b/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone new file mode 100644 index 00000000..9b6b86ee --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/d.f.ip6.arpa.zone @@ -0,0 +1,8 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 1 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/forwarder.toml b/hickory-dns/files/etc/hickory-dns/forwarder.toml index 98729bdc..dd95efd3 100644 --- a/hickory-dns/files/etc/hickory-dns/forwarder.toml +++ b/hickory-dns/files/etc/hickory-dns/forwarder.toml @@ -47,6 +47,57 @@ key_file = "/etc/hickory-dns/update.key" algorithm = "hmac-sha256" fudge = 300 +[[zones]] +zone = "d.f.ip6.arpa" +zone_type = "Primary" +axfr_policy = "AllowAll" + +[zones.stores] +type = "sqlite" +zone_path = "/etc/hickory-dns/d.f.ip6.arpa.zone" +journal_path = ":memory:" +allow_update = true + +[[zones.stores.tsig_keys]] +name = "update-key." +key_file = "/etc/hickory-dns/update.key" +algorithm = "hmac-sha256" +fudge = 300 + +[[zones]] +zone = "0.0.10.in-addr.arpa" +zone_type = "Primary" +axfr_policy = "AllowAll" + +[zones.stores] +type = "sqlite" +zone_path = "/etc/hickory-dns/0.0.10.zone" +journal_path = ":memory:" +allow_update = true + +[[zones.stores.tsig_keys]] +name = "update-key." +key_file = "/etc/hickory-dns/update.key" +algorithm = "hmac-sha256" +fudge = 300 + +[[zones]] +zone = "3.168.192.in-addr.arpa" +zone_type = "Primary" +axfr_policy = "AllowAll" + +[zones.stores] +type = "sqlite" +zone_path = "/etc/hickory-dns/3.168.192.zone" +journal_path = ":memory:" +allow_update = true + +[[zones.stores.tsig_keys]] +name = "update-key." +key_file = "/etc/hickory-dns/update.key" +algorithm = "hmac-sha256" +fudge = 300 + [[zones]] zone = "." zone_type = "External" diff --git a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper b/hickory-dns/files/usr/bin/ipv6-neigh-wrapper index eb805065..66619c08 100644 --- a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper +++ b/hickory-dns/files/usr/bin/ipv6-neigh-wrapper @@ -11,7 +11,10 @@ i=0 while [ "$i" -lt "$RETRY" ]; do if ss -lnt | grep -q ':53'; then logger -t ipv6-neigh-wrapper "DNS port 53 ready, starting ipv6-neigh" - exec "$NEIGH_PROG" --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router --keepalive-gua + exec "$NEIGH_PROG" --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router --keepalive-gua \ + --ptr-ipv4-subnet 10.0.0.0/24 \ + --ptr-ipv4-subnet 192.168.3.0/24 \ + --ptr-ula fi i=$((i + 1)) sleep 1 diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index d48afe03..fd3574f3 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -227,16 +227,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -534,7 +524,13 @@ dependencies = [ ] [[package]] -name = "ip-neigh" +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipv6-neigh" version = "0.1.0" dependencies = [ "clap", @@ -552,12 +548,6 @@ dependencies = [ "ubus", ] -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -572,9 +562,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" dependencies = [ "jiff-static", "log", @@ -585,9 +575,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" dependencies = [ "proc-macro2", "quote", @@ -612,26 +602,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mio" @@ -706,9 +687,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "once_cell" @@ -726,29 +707,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "paste" version = "1.0.15" @@ -848,15 +806,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.12.3" @@ -927,12 +876,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "semver" version = "1.0.28" @@ -971,9 +914,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -988,16 +931,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "slab" version = "0.4.12" @@ -1153,9 +1086,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index 09479045..da40a7c6 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ip-neigh" +name = "ipv6-neigh" version = "0.1.0" edition = "2024" @@ -8,14 +8,14 @@ edition = "2024" [dependencies] rtnetlink = { version = "0.21.0" } netlink-sys = { version = "0.8" } -tokio = { version = "1.52.3", features = ["full"]} +tokio = { version = "1.52.3", default-features = false, features = ["rt", "macros", "net", "time", "io-util"] } futures = "0.3.32" netlink-packet-route = {version = "0.30" } netlink-packet-core= {version ="0.8"} clap = { version = "4.6", default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] } hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "ee7ba64757eca61d86f323ef992eba482605d7c3", features = ["dnssec-ring"] } ubus = "0.1.7" -serde_json = "1.0.149" +serde_json = "1.0.150" log = "0.4" env_logger = "0.11" socket2 = { version = "0.6", features = ["all"] } @@ -25,3 +25,5 @@ opt-level = 3 lto = true codegen-units = 1 strip = true +overflow-checks = false +panic = "abort" diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index e1954023..0ccf770e 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:=15 +PKG_RELEASE:=16 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 @@ -12,6 +12,16 @@ RUST_PKG_LOCKED:=0 include $(INCLUDE_DIR)/package.mk include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk +# Derive Rust CPU flags from TARGET_CFLAGS (e.g. -mcpu=cortex-a53+crc+crypto) +_comma:=, +_empty:= +_space:=$(_empty) $(_empty) +_mcpu:=$(patsubst -mcpu=%,%,$(filter -mcpu=%,$(TARGET_CFLAGS))) +_mcpu_parts:=$(subst +,$(_space),$(_mcpu)) +_cpu_base:=$(firstword $(_mcpu_parts)) +_cpu_feats:=$(subst $(_space),$(_comma),$(addprefix +,$(wordlist 2,99,$(_mcpu_parts)))) +PKG_RUST_CPU_FLAGS:=$(if $(_cpu_base),-Ctarget-cpu=$(_cpu_base)$(if $(_cpu_feats), -Ctarget-feature=$(_cpu_feats))) + define Package/ipv6-neigh SECTION:=net CATEGORY:=Network @@ -37,6 +47,10 @@ endef define Build/Compile +$(CARGO_PKG_VARS) \ + CARGO_PROFILE_RELEASE_OPT_LEVEL=3 \ + CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS=false \ + CARGO_PROFILE_RELEASE_PANIC=abort \ + RUSTFLAGS="$(CARGO_RUSTFLAGS) $(PKG_RUST_CPU_FLAGS)" \ cargo build -v --profile $(CARGO_PKG_PROFILE) \ --manifest-path "$(PKG_BUILD_DIR)/Cargo.toml" \ --target $(RUSTC_TARGET_ARCH) \ @@ -44,12 +58,12 @@ define Build/Compile $(if $(filter --jobserver%,$(PKG_JOBS)),,-j1) \ $(CARGO_PKG_ARGS) +$(INSTALL_DIR) $(PKG_INSTALL_DIR)/bin/ - +$(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/$(CARGO_PKG_PROFILE)/ip-neigh $(PKG_INSTALL_DIR)/bin/ + +$(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/$(CARGO_PKG_PROFILE)/ipv6-neigh $(PKG_INSTALL_DIR)/bin/ 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_BIN) $(PKG_INSTALL_DIR)/bin/ipv6-neigh $(1)/usr/bin/ipv6-neigh endef $(eval $(call RustBinPackage,ipv6-neigh)) diff --git a/ipv6-neigh/src/db.rs b/ipv6-neigh/src/db.rs index b824549a..f851832b 100644 --- a/ipv6-neigh/src/db.rs +++ b/ipv6-neigh/src/db.rs @@ -2,7 +2,8 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use hickory_proto::op::{Message, Query, ResponseCode, update_message}; -use hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner}; +use hickory_proto::rr::{Name, RData, Record, RecordSet, RecordType, TSigner}; +use hickory_proto::rr::rdata::PTR as PtrRData; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::timeout; @@ -23,11 +24,100 @@ pub(crate) struct DnsUpdater { server_addr: SocketAddr, zone: Name, signer: TSigner, + /// (network as u32, prefix_len, reverse zone name) for IPv4 PTR + ipv4_ptr_zones: Vec<(u32, u8, Name)>, + /// Reverse zone for ULA IPv6 (d.f.ip6.arpa) + ula_ptr_zone: Option, } impl DnsUpdater { pub fn new(server_addr: SocketAddr, zone: Name, signer: TSigner) -> Self { - Self { server_addr, zone, signer } + Self { server_addr, zone, signer, ipv4_ptr_zones: vec![], ula_ptr_zone: None } + } + + /// Configure reverse PTR zones. + /// `ipv4_subnets`: list of (network_addr, prefix_len); /8, /16, /24 boundaries supported. + /// `ula`: if true, enables ULA PTR via `d.f.ip6.arpa`. + pub fn with_ptr_zones(mut self, ipv4_subnets: &[(Ipv4Addr, u8)], ula: bool) -> Self { + self.ipv4_ptr_zones = ipv4_subnets + .iter() + .map(|(net, prefix_len)| (u32::from(*net), *prefix_len, ipv4_zone_name(*net, *prefix_len))) + .collect(); + if ula { + self.ula_ptr_zone = Some(Name::from_ascii("d.f.ip6.arpa.").expect("always valid")); + } + self + } + + /// Upsert a PTR record. Silently skips IPs with no configured reverse zone. + pub async fn upsert_ptr( + &self, + addr: IpAddr, + hostname: &str, + ttl: u32, + ) -> Result<(), Box> { + let (ptr_name, rev_zone) = match addr { + IpAddr::V4(ip) => { + let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) }; + (ipv4_ptr_name(ip), zone) + } + IpAddr::V6(ip) if (ip.segments()[0] & 0xfe00) == 0xfc00 => { + let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) }; + (ipv6_ptr_name(ip), zone) + } + _ => return Ok(()), + }; + let target = Name::from_ascii(hostname)?.append_domain(&self.zone)?; + + // Delete any existing PTR rrset first (replace semantics: one PTR per IP). + let del_record = Record::update0(ptr_name.clone(), 0, RecordType::PTR); + let del_msg = update_message::delete_rrset(del_record, rev_zone.clone(), false); + let del_resp = self.send_tcp(del_msg).await?; + check_response(&del_resp, &[])?; + + // Add the new PTR. + let mut rrset = RecordSet::with_ttl(ptr_name, RecordType::PTR, ttl); + rrset.add_rdata(RData::PTR(PtrRData(target))); + let msg = update_message::append(rrset, rev_zone, false, false); + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; + Ok(()) + } + + /// Delete a PTR record. Silently skips IPs with no configured reverse zone. + pub async fn delete_ptr( + &self, + addr: IpAddr, + ) -> Result<(), Box> { + let (ptr_name, rev_zone) = match addr { + IpAddr::V4(ip) => { + let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) }; + (ipv4_ptr_name(ip), zone) + } + IpAddr::V6(ip) if (ip.segments()[0] & 0xfe00) == 0xfc00 => { + let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) }; + (ipv6_ptr_name(ip), zone) + } + _ => return Ok(()), + }; + let record = Record::update0(ptr_name, 0, RecordType::PTR); + let msg = update_message::delete_rrset(record, rev_zone, false); + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; + Ok(()) + } + + fn find_ipv4_ptr_zone(&self, addr: Ipv4Addr) -> Option<&Name> { + let addr_u32 = u32::from(addr); + self.ipv4_ptr_zones + .iter() + .filter(|(net, prefix_len, _)| { + if *prefix_len == 0 { return true; } + let shift = 32u8.saturating_sub(*prefix_len); + (addr_u32 >> shift) == (net >> shift) + }) + .max_by_key(|(_, prefix_len, _)| *prefix_len) + .map(|(_, _, name)| name) } /// Create or append a AAAA record. @@ -214,6 +304,41 @@ impl DnsUpdater { } } +/// Build the PTR owner name for an IPv4 address. +/// e.g. 192.168.3.5 → `5.3.168.192.in-addr.arpa.` +fn ipv4_ptr_name(addr: Ipv4Addr) -> Name { + let o = addr.octets(); + Name::from_ascii(&format!("{}.{}.{}.{}.in-addr.arpa.", o[3], o[2], o[1], o[0])) + .expect("always valid") +} + +/// Build the PTR owner name for an IPv6 address (nibble-reversed). +fn ipv6_ptr_name(addr: Ipv6Addr) -> Name { + let nibbles: String = addr + .octets() + .iter() + .rev() + .flat_map(|b| { + let lo = char::from_digit((b & 0x0f) as u32, 16).unwrap(); + let hi = char::from_digit((b >> 4) as u32, 16).unwrap(); + [lo, '.', hi, '.'] + }) + .collect(); + Name::from_ascii(&format!("{}ip6.arpa.", nibbles)).expect("always valid") +} + +/// Derive the reverse zone name for an IPv4 subnet (only /8, /16, /24 boundaries). +fn ipv4_zone_name(net: Ipv4Addr, prefix_len: u8) -> Name { + let o = net.octets(); + let s = match prefix_len { + 24..=32 => format!("{}.{}.{}.in-addr.arpa.", o[2], o[1], o[0]), + 16..=23 => format!("{}.{}.in-addr.arpa.", o[1], o[0]), + 8..=15 => format!("{}.in-addr.arpa.", o[0]), + _ => "in-addr.arpa.".to_string(), + }; + Name::from_ascii(&s).expect("always valid") +} + /// Extract the single label that precedes the zone apex from a fully-qualified record name. /// /// e.g. `"foo.lan."` with zone `"lan."` → `Some("foo")`. diff --git a/ipv6-neigh/src/iface.rs b/ipv6-neigh/src/iface.rs index 7303bc8a..a5614acc 100644 --- a/ipv6-neigh/src/iface.rs +++ b/ipv6-neigh/src/iface.rs @@ -112,6 +112,9 @@ pub(crate) async fn register_router_addresses( Err(e) => warn!("failed to register router AAAA {} for {}: {}", addr, name, e), } } + if let Err(e) = updater.upsert_ptr(IpAddr::V6(*addr), hostname, DEFAULT_TTL).await { + warn!("failed to register router PTR for {}: {}", addr, e); + } } IpAddr::V4(addr) => { for name in std::iter::once(hostname).chain(extra_alias) { @@ -120,6 +123,9 @@ pub(crate) async fn register_router_addresses( Err(e) => warn!("failed to register router A {} for {}: {}", addr, name, e), } } + if let Err(e) = updater.upsert_ptr(IpAddr::V4(*addr), hostname, DEFAULT_TTL).await { + warn!("failed to register router PTR for {}: {}", addr, e); + } } } } diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index f70b5016..dda9f021 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::net::SocketAddr as StdSocketAddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr as StdSocketAddr}; use std::time::Instant; use futures::stream::StreamExt; @@ -80,6 +80,12 @@ struct Cli { /// Maximum number of ULA AAAA records to publish per host (oldest pruned first) #[clap(long, default_value = "2")] max_ula_per_host: usize, + /// IPv4 subnets for PTR reverse DNS (CIDR notation, e.g. "192.168.3.0/24", repeatable) + #[clap(long, value_name = "SUBNET")] + ptr_ipv4_subnet: Vec, + /// Enable PTR reverse DNS for ULA IPv6 addresses (fc00::/7) via d.f.ip6.arpa + #[clap(long)] + ptr_ula: bool, } @@ -96,6 +102,22 @@ fn is_failed_state(state: NeighbourState) -> bool { matches!(state, NeighbourState::Failed) } +/// Parse an IPv4 CIDR string like "192.168.3.0/24" into (network_addr, prefix_len). +/// Panics with a clear message on invalid input. +fn parse_ipv4_cidr(s: &str) -> (Ipv4Addr, u8) { + let (addr_str, prefix_str) = s.split_once('/').unwrap_or_else(|| { + panic!("invalid --ptr-ipv4-subnet \"{s}\": expected CIDR notation like \"192.168.3.0/24\"") + }); + let addr: Ipv4Addr = addr_str.parse().unwrap_or_else(|_| { + panic!("invalid --ptr-ipv4-subnet \"{s}\": \"{addr_str}\" is not a valid IPv4 address") + }); + let prefix_len: u8 = prefix_str.parse().unwrap_or_else(|_| { + panic!("invalid --ptr-ipv4-subnet \"{s}\": prefix length must be 0–32") + }); + assert!(prefix_len <= 32, "invalid --ptr-ipv4-subnet \"{s}\": prefix length {prefix_len} > 32"); + (addr, prefix_len) +} + #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), ()> { let args = Cli::parse(); @@ -126,7 +148,12 @@ async fn main() -> Result<(), ()> { let signer = TSigner::new(key_data, TsigAlgorithm::HmacSha256, key_name, 300) .expect("invalid TSIG key"); - let updater = db::DnsUpdater::new(args.dns_server, zone, signer); + let ipv4_ptr_subnets: Vec<(Ipv4Addr, u8)> = args.ptr_ipv4_subnet + .iter() + .map(|s| parse_ipv4_cidr(s)) + .collect(); + let updater = db::DnsUpdater::new(args.dns_server, zone, signer) + .with_ptr_zones(&ipv4_ptr_subnets, args.ptr_ula); // Wait for the DNS server to become available before sending any updates. wait_for_dns_server(args.dns_server).await; @@ -274,6 +301,16 @@ async fn main() -> Result<(), ()> { match result { Ok(()) => { info!("DNS update: added {} -> {:?}", hostname, neigh.inet); + let ip = match &neigh.inet { + NeighbourAddress::Inet6(addr) => Some(IpAddr::V6(*addr)), + NeighbourAddress::Inet(addr) => Some(IpAddr::V4(*addr)), + _ => None, + }; + if let Some(ip) = ip { + if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await { + warn!("PTR update failed for {}: {}", hostname, e); + } + } registered.insert(key, RegisteredEntry { hostname: hostname.clone(), last_confirmed: Instant::now(), @@ -416,18 +453,22 @@ async fn main() -> Result<(), ()> { // Resolve key by hostname (handles MAC changes: new MAC in leases // can now evict entries registered under an old MAC). // Fall back to scanning registered by IP if MAC is no longer in leases. + debug!("Neighbour failed: {:?}", neigh); let key_opt: Option<(String, String)> = leases .get(&neigh.mac) .map(|h| (h.clone(), ip_str.clone())) .or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned()); if let Some(key) = key_opt { if let Some(entry) = registered.remove(&key) { - debug!("Neighbour failed: {:?}", neigh); if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await { // DNS delete failed, put back so we retry registered.insert(key, entry); } + } else { + debug!("Neighbour failed: not in registered map, key={:?}", key); } + } else { + debug!("Neighbour failed: no registered key for ip={}", ip_str); } } else if neigh.state == NeighbourState::Reachable { if let Some(hostname) = leases.get(&neigh.mac) { diff --git a/ipv6-neigh/src/probe.rs b/ipv6-neigh/src/probe.rs index df519d86..88af518a 100644 --- a/ipv6-neigh/src/probe.rs +++ b/ipv6-neigh/src/probe.rs @@ -33,12 +33,12 @@ fn compute_icmpv4_checksum(packet: &mut [u8]) { } pub(crate) 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::RAW, Some(Protocol::ICMPV6))?; socket.set_nonblocking(true)?; // Bind outgoing packet to the specific interface via IPV6_UNICAST_IF so the // kernel NUD state machine updates the correct neighbour entry. set_ipv6_unicast_if(&socket, ifindex)?; - // ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1 + // ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes for RAW ICMPV6), id=0, seq=1 let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1]; let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0)); socket.send_to(&packet, &dest)?; @@ -46,7 +46,7 @@ pub(crate) fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result< } pub(crate) 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::RAW, Some(Protocol::ICMPV4))?; socket.set_nonblocking(true)?; // Bind outgoing packet to the specific interface via IP_UNICAST_IF. set_ip_unicast_if(&socket, ifindex)?; diff --git a/ipv6-neigh/src/reconcile.rs b/ipv6-neigh/src/reconcile.rs index 51575075..851fbcf8 100644 --- a/ipv6-neigh/src/reconcile.rs +++ b/ipv6-neigh/src/reconcile.rs @@ -35,6 +35,14 @@ pub(crate) async fn process_new_neigh( match result { Ok(()) => { info!("DNS update: added {} -> {:?}", hostname, neigh.inet); + let ip = match &neigh.inet { + NeighbourAddress::Inet6(addr) => IpAddr::V6(*addr), + NeighbourAddress::Inet(addr) => IpAddr::V4(*addr), + _ => return true, + }; + if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await { + warn!("PTR update failed for {} ({}): {}", hostname, ip, e); + } true } Err(e) => { @@ -58,6 +66,14 @@ pub(crate) async fn do_delete_dns( match result { Ok(()) => { info!("DNS update: removed {} -> {:?}", hostname, inet); + let ip = match inet { + NeighbourAddress::Inet6(addr) => IpAddr::V6(*addr), + NeighbourAddress::Inet(addr) => IpAddr::V4(*addr), + _ => return true, + }; + if let Err(e) = updater.delete_ptr(ip).await { + warn!("PTR delete failed for {} ({}): {}", hostname, ip, e); + } true } Err(e) => { @@ -172,7 +188,12 @@ pub(crate) async fn reconcile_dns( IpAddr::V4(addr) => updater.delete_a(hostname, *addr).await, }; match result { - Ok(()) => info!("reconcile: deleted stale DNS {} -> {}", hostname, ip), + Ok(()) => { + info!("reconcile: deleted stale DNS {} -> {}", hostname, ip); + if let Err(e) = updater.delete_ptr(*ip).await { + warn!("reconcile: PTR delete failed for stale {} {}: {}", hostname, ip, e); + } + } Err(e) => warn!("reconcile: failed to delete stale DNS {} {}: {}", hostname, ip, e), } } @@ -198,7 +219,14 @@ pub(crate) async fn reconcile_dns( _ => continue, }; match del_result { - Ok(()) => info!("reconcile: deleted orphan DNS {} -> {}", hostname, ip_str), + Ok(()) => { + info!("reconcile: deleted orphan DNS {} -> {}", hostname, ip_str); + if let Ok(ip) = ip_str.parse::() { + if let Err(e) = updater.delete_ptr(ip).await { + warn!("reconcile: PTR delete failed for orphan {} {}: {}", hostname, ip_str, e); + } + } + } Err(e) => warn!("reconcile: failed to delete orphan {} {}: {}", hostname, ip_str, e), } // Probe so alive devices trigger a REACHABLE event and re-register. @@ -234,6 +262,11 @@ pub(crate) async fn reconcile_dns( match result { Ok(()) => { info!("reconcile: re-pushed {} -> {}", hostname, ip_str); + if let Ok(ip) = ip_str.parse::() { + if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await { + warn!("reconcile: PTR re-push failed for {} {}: {}", hostname, ip_str, e); + } + } entry.last_confirmed = Instant::now(); } Err(e) => warn!("reconcile: failed to re-push {} {}: {}", hostname, ip_str, e), diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 00efa2d1..3f25abd5 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.5.20 -PKG_RELEASE:=138 +PKG_RELEASE:=139 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua b/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua index ae7b46da..818d8fdc 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_smartdns_add.lua @@ -194,7 +194,7 @@ config_lines = { tonumber(LISTEN_PORT) ~= 0 and "bind [::]:" .. LISTEN_PORT .. "@lo" or "", (tonumber(LOCAL_PORT) ~= 0 and LOCAL_GROUP) and "bind [::]:" .. LOCAL_PORT .. "@lo -group " .. LOCAL_GROUP or "", tonumber(force_https_soa) == 1 and "force-qtype-SOA 65" or "force-qtype-SOA -,65", - "server 114.114.114.114 -bootstrap-dns", + "server 223.5.5.5 -bootstrap-dns", is_file_nonzero("/etc/hosts") and "hosts-file /etc/hosts" or "", DNS_MODE == "socks" and string.format("proxy-server socks5://%s -name %s", REMOTE_PROXY_SERVER, proxy_server_name) or "" } diff --git a/luci-app-ssw/Makefile b/luci-app-ssw/Makefile index d9670585..877b58e9 100644 --- a/luci-app-ssw/Makefile +++ b/luci-app-ssw/Makefile @@ -10,8 +10,8 @@ LUCI_TITLE:=Luci app for ssw LUCI_DEPENDS:= +luci-app-modeminfo PKG_LICENSE:=GPLv3 -PKG_VERSION:=0.0.1 -PKG_RELEASE:=1 +PKG_VERSION:=0.0.2 +PKG_RELEASE:=2 include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-ssw/htdocs/luci-static/resources/view/modem/ssw.js b/luci-app-ssw/htdocs/luci-static/resources/view/modem/ssw.js index 9e539c77..e6bc5af6 100644 --- a/luci-app-ssw/htdocs/luci-static/resources/view/modem/ssw.js +++ b/luci-app-ssw/htdocs/luci-static/resources/view/modem/ssw.js @@ -37,9 +37,9 @@ return view.extend({ _('Revert to default sim slot. Each failed attempt doubles revert time.')); o.depends({enable: '1'}); - o = s.option(form.ListValue, 'rsrp', _('RSRP value'), - _('Switch sim lower by value.')); - for (var rsrp = -120; rsrp <= -80; rsrp++) { + o = s.option(form.ListValue, 'rsrp', _('RSRP/RSCP value'), + _('Switch sim lower by value. For 4G/5G is RSRP, for 3G RSCP.')); + for (var rsrp = -120; rsrp <= -50; rsrp++) { o.value(rsrp, rsrp +' '+ _('dBm')); }; o.depends({enable: '1'}); diff --git a/luci-app-ssw/po/ru/ssw.po b/luci-app-ssw/po/ru/ssw.po index 53f07a5c..f4d54385 100644 --- a/luci-app-ssw/po/ru/ssw.po +++ b/luci-app-ssw/po/ru/ssw.po @@ -22,11 +22,11 @@ msgstr "Вернуть" msgid "Revert to default sim slot. Each failed attempt doubles revert time." msgstr "Вернуть на SIM слот по-умолчанию. Каждая неудачная попытка увеличивает время возврата." -msgid "RSRP value" -msgstr "Значение RSRP" +msgid "RSRP/RSCP value" +msgstr "Значение RSRP/RSCP" -msgid "Switch sim lower by value." -msgstr "Переключить sim ниже значения." +msgid "Switch sim lower by value. For 4G/5G is RSRP, for 3G RSCP." +msgstr "Переключить sim ниже значения. Для 4G/5G значение RSRP, для 3G - RSCP." msgid "Interval check. sec" msgstr "Интервал проверки. сек" diff --git a/luci-app-tinyfilemanager/Makefile b/luci-app-tinyfilemanager/Makefile index 7f5a33ce..999eacac 100644 --- a/luci-app-tinyfilemanager/Makefile +++ b/luci-app-tinyfilemanager/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk LUCI_NAME:=luci-app-tinyfilemanager PKG_VERSION:=2.6 -PKG_RELEASE:=1 +PKG_RELEASE:=2 LUCI_TITLE:=LuCI Tiny File Manager LUCI_DEPENDS:=+php8 +php8-cgi +php8-fastcgi +php8-fpm +php8-mod-session +php8-mod-ctype +php8-mod-fileinfo +php8-mod-zip +php8-mod-iconv +php8-mod-mbstring +coreutils-stat +zoneinfo-asia +bash +curl +tar +inotifywait diff --git a/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations b/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations index d580341f..f25e5bbe 100644 --- a/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations +++ b/luci-app-tinyfilemanager/root/etc/nginx/conf.d/tinyfilemanager.locations @@ -1,33 +1,33 @@ -location = /tinyfilemanager/ { - fastcgi_split_path_info ^(.+?\.php)(/.*)$; - if (!-f $document_root$fastcgi_script_name) { - return 404; +location /tinyfilemanager/ { + index index.php; + + location ~ \.php$ { + try_files $uri =404; + + # Mitigate https://httpoxy.org/ vulnerabilities + fastcgi_param HTTP_PROXY ""; + + #error_log /dev/null; + fastcgi_connect_timeout 300s; + fastcgi_read_timeout 300s; + fastcgi_send_timeout 300s; + fastcgi_buffer_size 32k; + fastcgi_buffers 4 32k; + fastcgi_busy_buffers_size 32k; + fastcgi_temp_file_write_size 32k; + client_body_timeout 10s; + send_timeout 60s; # default, increase if experiencing a lot of timeouts. + output_buffers 1 32k; + include fastcgi_params; + + # Only throw it at PHP-FPM if file exists (prevents PHP exploits). + fastcgi_pass 127.0.0.1:1026; # or: unix:/var/run/php-fpm.sock; + # fastcgi_pass unix:/var/run/php8-fpm.sock; + + # SCRIPT_FILENAME parameter is used for PHP FPM determining + # the script name. If it is not set in fastcgi_params file, + # i.e. /etc/nginx/fastcgi_params or in the parent contexts, + # please comment off following line: + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } - - # Mitigate https://httpoxy.org/ vulnerabilities - fastcgi_param HTTP_PROXY ""; - - #error_log /dev/null; - fastcgi_connect_timeout 300s; - fastcgi_read_timeout 300s; - fastcgi_send_timeout 300s; - fastcgi_buffer_size 32k; - fastcgi_buffers 4 32k; - fastcgi_busy_buffers_size 32k; - fastcgi_temp_file_write_size 32k; - client_body_timeout 10s; - send_timeout 60s; # default, increase if experiencing a lot of timeouts. - output_buffers 1 32k; - fastcgi_index index.php; - include fastcgi_params; - - # Only throw it at PHP-FPM if file exists (prevents PHP exploits). - fastcgi_pass 127.0.0.1:1026; # or: unix:/var/run/php-fpm.sock; - # fastcgi_pass unix:/var/run/php8-fpm.sock; - - # SCRIPT_FILENAME parameter is used for PHP FPM determining - # the script name. If it is not set in fastcgi_params file, - # i.e. /etc/nginx/fastcgi_params or in the parent contexts, - # please comment off following line: - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } diff --git a/ssw/Makefile b/ssw/Makefile index 17675e27..42849e41 100644 --- a/ssw/Makefile +++ b/ssw/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ssw PKG_VERSION:=0.0.2 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_MAINTAINER:=Konstantine Shevlakov include $(INCLUDE_DIR)/package.mk diff --git a/ssw/root/etc/init.d/ssw b/ssw/root/etc/init.d/ssw index 0d699386..84449b49 100755 --- a/ssw/root/etc/init.d/ssw +++ b/ssw/root/etc/init.d/ssw @@ -2,6 +2,7 @@ # SIM slot switcher for HuastLink HC-G60 or ZBT-WE2806-A # by Konstantine Shevlakov at 2025 +# Audited & fixed 2025 START=99 STOP=10 @@ -11,7 +12,6 @@ load_config_gpio(){ local gpio value for c in gpio value; do config_get $1_$c "$1" $c - export $1_$c done } @@ -19,54 +19,58 @@ load_config_failover(){ local enable mode interval times_rsrp times_ping revert rsrp host for c in enable mode interval times_rsrp times_ping revert rsrp host; do config_get $1_$c "$1" $c - export $1_$c done } reload_iface(){ - [ "$iface" ] && { + [ "$iface" ] && { for i in $iface; do - ifup $i + ifup "$i" done } } +_apply_modem_state(){ + echo "$sim_value" > /sys/class/gpio/"$sim_gpio"/value + echo "$modem_value" > /sys/class/gpio/"$modem_gpio"/value + if [ -x /etc/init.d/smstools3 ]; then + /etc/init.d/smstools3 restart + fi + sleep 30 && reload_iface & +} + start_service(){ config_load ssw config_foreach load_config_gpio modem config_foreach load_config_gpio sim - modem_cur=$(cat /sys/class/gpio/$modem_gpio/value) - sim_cur=$(cat /sys/class/gpio/$sim_gpio/value) - iface=$(uci show network | awk -F [.] '/devices/{gsub("'\''","");print $2}' | tail -1) - if [ "$modem_value" = "$modem_cur" -a "$sim_value" = "$sim_cur" ]; then - continue - elif [ "$modem_value" = "$modem_cur" -a "$modem_value" = "1" ]; then - logger -t "SIM SW" "modem already enabled on gpio $modem_gpio. Restart it" - echo "0" > /sys/class/gpio/$modem_gpio/value + + local modem_cur sim_cur + modem_cur=$(cat /sys/class/gpio/"$modem_gpio"/value) + sim_cur=$(cat /sys/class/gpio/"$sim_gpio"/value) + + iface=$(uci show network | awk -F'[.]' '/devices/{gsub("'\''",""); print $2}' | tail -1) + + if [ "$modem_value" = "$modem_cur" ] && [ "$sim_value" = "$sim_cur" ]; then + logger -t "SIM SW" "Modem and SIM already in correct state. Nothing to do." + return 0 + elif [ "$modem_value" = "$modem_cur" ] && [ "$modem_value" = "1" ]; then + logger -t "SIM SW" "Modem already enabled on gpio $modem_gpio. Restart it." + echo "0" > /sys/class/gpio/"$modem_gpio"/value sleep 2 - echo "$sim_value" > /sys/class/gpio/$sim_gpio/value - echo "$modem_value" > /sys/class/gpio/$modem_gpio/value - if [ -x /etc/init.d/smstools3 ]; then - /etc/init.d/smstools3 restart - fi - sleep 30 && reload_iface & + _apply_modem_state else - logger -t "SIM SW" "modem powered changed" - echo "$sim_value" > /sys/class/gpio/$sim_gpio/value - echo "$modem_value" > /sys/class/gpio/$modem_gpio/value - if [ -x /etc/init.d/smstools3 ]; then - /etc/init.d/smstools3 restart - fi - sleep 30 && reload_iface & + logger -t "SIM SW" "Modem power state changed." + _apply_modem_state fi - #config_foreach load_config_failover failover + procd_open_instance procd_set_param command /usr/share/ssw_track.sh procd_close_instance } + reload_service(){ - start + start_service } service_triggers(){ diff --git a/ssw/root/usr/share/ssw_track.sh b/ssw/root/usr/share/ssw_track.sh index 8107a10a..64afa73e 100755 --- a/ssw/root/usr/share/ssw_track.sh +++ b/ssw/root/usr/share/ssw_track.sh @@ -1,8 +1,9 @@ #!/bin/sh # SIM slot switcher for HuastLink HC-G60 or ZBT-WE2806-A -# or similiar cpe OpenWrt-routers +# or similar CPE OpenWrt-routers # by Konstantine Shevlakov at 2025 +# Audited & fixed 2026 NODE="SW SIM" @@ -34,38 +35,58 @@ NODE="SW SIM" # option value '1' -# Get Variables +. /lib/functions.sh + +# Callback failover section +_load_failover(){ + config_get enable "$1" enable "" + config_get interval "$1" interval 60 + config_get revert "$1" revert "" + config_get rsrp "$1" rsrp "" + config_get times_rsrp "$1" times_rsrp 5 + config_get apn1 "$1" apn1 "" + config_get apn2 "$1" apn2 "" +} + +# Callback sections modem / sim (gpio type) +_load_gpio(){ + local _gpio _value + config_get _gpio "$1" gpio "" + config_get _value "$1" value "" + eval "${1}_gpio=\"$_gpio\"" + eval "${1}_value=\"$_value\"" +} + +# Get Variables — update for every cycle get_vars(){ - for v in enable interval revert rsrp times_rsrp apn1 apn2; do - eval $v=$(uci -q get ssw.failover.${v} 2>/dev/nul) - done - for d in modem sim; do - for s in gpio value; do - eval "${d}_${s}=$(uci -q get ssw.${d}.${s} 2>/dev/null)" - done - done - [ -n "$interval" ] || interval=60 - [ -n "$times_rsrp" ] || times_rsrp=5 + reset_cb + config_load ssw + config_foreach _load_failover failover + config_foreach _load_gpio modem + config_foreach _load_gpio sim } # SIM Switch sw_sim(){ - case $cur_sim in + case "$cur_sim" in 0) next_sim=1 ;; 1) next_sim=0 ;; + *) + logger -t "$NODE" "ERROR: cur_sim has unexpected value '$cur_sim'. Aborting sw_sim." + return 1 + ;; esac - echo "0" > /sys/class/gpio/$modem_gpio/value - echo "$next_sim" > /sys/class/gpio/$sim_gpio/value + echo "0" > /sys/class/gpio/"$modem_gpio"/value + echo "$next_sim" > /sys/class/gpio/"$sim_gpio"/value if [ "$modem_value" = "1" ]; then sleep 2 - echo "1" > /sys/class/gpio/$modem_gpio/value + echo "1" > /sys/class/gpio/"$modem_gpio"/value if [ -x /etc/init.d/smstools3 ]; then /etc/init.d/smstools3 restart fi fi - set > /tmp/apn.ssw } # Revert rule switch @@ -84,32 +105,59 @@ sw_rule(){ # Check interface state via mwan3 monitor_mwan3(){ - iface=$(uci show network | awk -F [.] '/devices/{print $2}') + iface=$(uci show network | awk -F'[.]' '/devices/{print $2}' | tail -1) # Check link status - if [ -r /tmp/run/mwan3/iface_state/$iface ]; then - link_status=$(cat /tmp/run/mwan3/iface_state/$iface | grep online | wc -l) + if [ -r "/tmp/run/mwan3/iface_state/$iface" ]; then + link_status=$(grep -c online "/tmp/run/mwan3/iface_state/$iface" || echo 0) else # Disable track link via mwan3 link_status=1 fi } -# RSRP average value by modemmanager. Enable singnal monitor! +check_signal(){ + local gen="$1" signal_json="$2" + local metric value + + case "$gen" in + lte|5g) metric="rsrp" ;; + 3g) metric="rscp" ;; + *) + logger -t "$NODE" "check_signal: unknown generation '$gen'" + return 1 + ;; + esac + + value=$(echo "$signal_json" \ + | jsonfilter -e "@[\"modem\"][*][\"${gen}\"][\"${metric}\"]" 2>/dev/null \ + | awk '{printf "%.0f\n", $1}') + + [ -n "$value" ] && echo "$value" +} + +# RSRP/RSCP average value by modemmanager. Enable signal monitor! monitor_rsrp(){ - device="$(uci show network | awk -F [=] '/devices/{gsub("'\''","");print $2}')" - SIGNAL="$(mmcli -J -m $device --signal-get)" - CRSRP=$(echo "$SIGNAL" | jsonfilter -e '@["modem"][*]["lte"]["rsrp"]' | awk '{printf "%.0f\n", $1}') - if ! [ $CRSRP ]; then - CRSRP=$(echo "$SIGNAL" | jsonfilter -e '@["modem"][*]["5g"]["rsrp"]' | awk '{printf "%.0f\n", $1}') + local device SIGNAL CRSRP + + device="$(uci show network | awk -F'[=]' '/devices/{gsub("'\''",""); print $2}' | tail -1)" + SIGNAL="$(mmcli -J -m "$device" --signal-get 2>/dev/null)" + + for gen in lte 5g 3g; do + CRSRP=$(check_signal "$gen" "$SIGNAL") + [ -n "$CRSRP" ] && break + done + + if [ -n "$CRSRP" ] && [ "$CRSRP" -ne 0 ]; then + echo "$CRSRP" >> /tmp/ssw_rsrp.var fi - if [ $CRSRP -ne 0 ]; then - echo $CRSRP >> /tmp/ssw_rsrp.var - fi - if [ $cnt -eq $times_rsrp ]; then - RSRP=$(awk '{sum+=$1} END { printf "%.0f\n", sum/NR }' /tmp/ssw_rsrp.var) - cat /dev/null > /tmp/ssw_rsrp.var - if [ $RSRP -lt $rsrp ]; then + if [ "$cnt" -eq "$times_rsrp" ]; then + if [ -s /tmp/ssw_rsrp.var ]; then + RSRP=$(awk '{sum+=$1} END { printf "%.0f\n", sum/NR }' /tmp/ssw_rsrp.var) + fi + : > /tmp/ssw_rsrp.var + + if [ -n "$RSRP" ] && [ "$RSRP" -lt "$rsrp" ]; then mon_rsrp=0 else mon_rsrp=1 @@ -121,68 +169,74 @@ monitor_rsrp(){ reload_iface(){ [ "$iface" ] && { for i in $iface; do - ifup $i + ifup "$i" done } } -# Stuff -cnt=1 +cnt=0 + while true; do get_vars - sleep $interval + sleep "$interval" + if [ "$enable" = "1" ]; then - cur_sim=$(cat /sys/class/gpio/$sim_gpio/value) + cur_sim=$(cat /sys/class/gpio/"$sim_gpio"/value) + + cnt=$(( cnt + 1 )) monitor_rsrp monitor_mwan3 + if [ "$cnt" -eq "$times_rsrp" ]; then - if [ "$link_status" = "0" -o "$mon_rsrp" = "0" ]; then + if [ "$link_status" = "0" ] || [ "$mon_rsrp" = "0" ]; then if [ "$sim_value" -eq "$cur_sim" ]; then apn=$apn2 else apn=$apn1 fi - iface=$(uci show network | awk -F [.] '/devices/{gsub("'\''","");print $2}' | tail -1) - if [ $RSRP ]; then + + iface=$(uci show network | awk -F'[.]' '/devices/{gsub("'\''",""); print $2}' | tail -1) + + if [ -n "$RSRP" ]; then if [ "$mon_rsrp" = "0" ]; then - logger -t "$NODE" "Modem interface: $iface is average RSRP= ${RSRP} dBm. Min. value ${rsrp} dBm." + logger -t "$NODE" "Modem interface: $iface average RSRP=${RSRP} dBm. Min. value ${rsrp} dBm." fi if [ "$link_status" = "0" ]; then - logger -t "$NODE" "Modem interface: $iface is loss connectivity" + logger -t "$NODE" "Modem interface: $iface lost connectivity." fi else - logger -t "$NODE" "WARNING: RSRP value not exist. Please check \"Resfesh signal\" option of modem interface!" + logger -t "$NODE" "WARNING: RSRP value not available. Check 'Refresh signal' option of modem interface!" fi - if [ "${#apn}" -gt "0" ]; then - uci set network.$iface.apn="$apn" + + if [ "${#apn}" -gt 0 ]; then + uci set network."$iface".apn="$apn" logger -t "$NODE" "Switch SIM-card slot with APN: $apn" else - logger -t "$NODE" "WARNING: APN not defined. Switch SIM-card slot with default APN." - uci set network.$iface.apn="internet" + logger -t "$NODE" "WARNING: APN not defined. Switching SIM-card slot with default APN." + uci set network."$iface".apn="internet" fi uci commit network - reload_config network + reload_config if [ "$revert" = "1" ]; then - if [ "$cur_sim" -eq "$sim_value" ]; then - FBT=${FBT:=$((($interval+$times_rsrp)*2))} - FBT=$(($FBT*2)) - SWDATE=$((`date +%s`+$FBT)) - echo "FBT=$FBT" > /tmp/ssw.vars - echo SWDATE=$SWDATE >> /tmp/ssw.vars - logger -t "$NODE" "Back to default SIM-slot after $(date -d @${SWDATE})" + if [ "$cur_sim" -eq "$sim_value" ]; then + FBT=${FBT:=$(( (interval + times_rsrp) * 2 ))} + FBT=$(( FBT * 2 )) + SWDATE=$(( $(date +%s) + FBT )) + printf 'FBT=%s\nSWDATE=%s\n' "$FBT" "$SWDATE" > /tmp/ssw.vars + logger -t "$NODE" "Back to default SIM-slot after $(date -d @"${SWDATE}")" fi fi + sw_sim && sleep 20 && reload_iface & - fi - if [ "$revert" = "1" ]; then + + elif [ "$revert" = "1" ]; then sw_rule && sleep 20 && reload_iface & fi - cnt=0 - else - cnt=$(($cnt+1)) + + cnt=1 fi fi -done +done \ No newline at end of file