From 524971471725e3e74462343a19b89858a1cc2b27 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 8 Aug 2025 03:01:18 +0200 Subject: [PATCH 1/3] Fix clippy lints and warnings in Rust 1.89 This will also require Rust 1.89 due to if-let. --- daemon/src/diag.rs | 9 +-- daemon/src/display/tplink_onebit.rs | 8 +- daemon/src/key_input.rs | 10 +-- installer/src/orbic.rs | 30 ++++---- lib/src/analysis/analyzer.rs | 4 +- .../analysis/connection_redirect_downgrade.rs | 4 +- lib/src/analysis/imsi_requested.rs | 4 +- lib/src/analysis/incomplete_sib.rs | 4 +- lib/src/analysis/nas_null_cipher.rs | 28 +++---- lib/src/analysis/null_cipher.rs | 28 +++---- lib/src/analysis/priority_2g_downgrade.rs | 74 +++++++++---------- lib/src/diag_device.rs | 8 +- lib/src/qmdl.rs | 18 ++--- 13 files changed, 113 insertions(+), 116 deletions(-) diff --git a/daemon/src/diag.rs b/daemon/src/diag.rs index 508a1b9..74b6ca9 100644 --- a/daemon/src/diag.rs +++ b/daemon/src/diag.rs @@ -94,16 +94,15 @@ impl DiagTask { /// Stop recording async fn stop(&mut self, qmdl_store: &mut RecordingStore) { self.stop_current_recording().await; - if let Some((_, entry)) = qmdl_store.get_current_entry() { - if let Err(e) = self + if let Some((_, entry)) = qmdl_store.get_current_entry() + && let Err(e) = self .analysis_sender .send(AnalysisCtrlMessage::RecordingFinished( entry.name.to_string(), )) .await - { - warn!("couldn't send analysis message: {e}"); - } + { + warn!("couldn't send analysis message: {e}"); } if let Err(e) = qmdl_store.close_current_entry().await { error!("couldn't close current entry: {e}"); diff --git a/daemon/src/display/tplink_onebit.rs b/daemon/src/display/tplink_onebit.rs index 3b55721..34aa48e 100644 --- a/daemon/src/display/tplink_onebit.rs +++ b/daemon/src/display/tplink_onebit.rs @@ -145,10 +145,10 @@ pub fn update_ui( // we write the status every second because it may have been overwritten through menu // navigation. - if display_level != 0 { - if let Err(e) = tokio::fs::write(OLED_PATH, pixels).await { - error!("failed to write to display: {e}"); - } + if display_level != 0 + && let Err(e) = tokio::fs::write(OLED_PATH, pixels).await + { + error!("failed to write to display: {e}"); } tokio::time::sleep(Duration::from_millis(1000)).await; diff --git a/daemon/src/key_input.rs b/daemon/src/key_input.rs index 25b18fc..2a66c8c 100644 --- a/daemon/src/key_input.rs +++ b/daemon/src/key_input.rs @@ -61,11 +61,11 @@ pub fn run_key_input_thread( // On orbic it was observed that pressing the power button can trigger many successive // events. Drop events that are too close together. - if let Some(last_time) = last_event_time { - if now.duration_since(last_time) < Duration::from_millis(50) { - last_event_time = Some(now); - continue; - } + if let Some(last_time) = last_event_time + && now.duration_since(last_time) < Duration::from_millis(50) + { + last_event_time = Some(now); + continue; } last_event_time = Some(now); diff --git a/installer/src/orbic.rs b/installer/src/orbic.rs index 16c5f36..aa3d375 100644 --- a/installer/src/orbic.rs +++ b/installer/src/orbic.rs @@ -174,10 +174,9 @@ pub async fn test_rayhunter(adb_device: &mut ADBUSBDevice) -> Result<()> { if let Ok(output) = adb_command( adb_device, &["wget", "-O", "-", "http://localhost:8080/index.html"], - ) { - if output.contains("html") { - return Ok(()); - } + ) && output.contains("html") + { + return Ok(()); } failures += 1; sleep(Duration::from_secs(3)).await; @@ -297,14 +296,12 @@ async fn adb_echo_test(mut adb_device: ADBUSBDevice) -> Result { Ok::<(ADBUSBDevice, Vec), RustADBError>((adb_device, buf)) }); sleep(Duration::from_secs(1)).await; - if thread.is_finished() { - if let Ok(Ok((dev, buf))) = thread.join() { - if let Ok(s) = std::str::from_utf8(&buf) { - if s.contains(test_echo) { - return Ok(dev); - } - } - } + if thread.is_finished() + && let Ok(Ok((dev, buf))) = thread.join() + && let Ok(s) = std::str::from_utf8(&buf) + && s.contains(test_echo) + { + return Ok(dev); } // I'd like to kill the background thread here if that was possible. bail!("Could not communicate with the Orbic. Try disconnecting and reconnecting."); @@ -317,10 +314,11 @@ async fn wait_for_usb_device(vendor_id: u16, product_id: u16) -> Result<()> { loop { let mut watcher = nusb::watch_devices()?; while let Some(event) = watcher.next().await { - if let HotplugEvent::Connected(dev) = event { - if dev.vendor_id() == vendor_id && dev.product_id() == product_id { - return Ok(()); - } + if let HotplugEvent::Connected(dev) = event + && dev.vendor_id() == vendor_id + && dev.product_id() == product_id + { + return Ok(()); } } } diff --git a/lib/src/analysis/analyzer.rs b/lib/src/analysis/analyzer.rs index e1c3794..64bea08 100644 --- a/lib/src/analysis/analyzer.rs +++ b/lib/src/analysis/analyzer.rs @@ -76,14 +76,14 @@ pub struct Event { /// many hours at a time with dozens of [Analyzers](Analyzer) working in parallel. pub trait Analyzer { /// Returns a user-friendly, concise name for your heuristic. - fn get_name(&self) -> Cow; + fn get_name(&self) -> Cow<'_, str>; /// Returns a user-friendly description of what your heuristic looks for, /// the types of [Events](Event) it may return, as well as possible false-positive /// conditions that may trigger an [Event]. If different [Events](Event) have /// different false-positive conditions, consider including them in its /// `message` field. - fn get_description(&self) -> Cow; + fn get_description(&self) -> Cow<'_, str>; /// Analyze a single [InformationElement], possibly returning an [Event] if your /// heuristic deems it relevant. Again, be mindful of any state your diff --git a/lib/src/analysis/connection_redirect_downgrade.rs b/lib/src/analysis/connection_redirect_downgrade.rs index 0f40fa5..d56228b 100644 --- a/lib/src/analysis/connection_redirect_downgrade.rs +++ b/lib/src/analysis/connection_redirect_downgrade.rs @@ -14,11 +14,11 @@ pub struct ConnectionRedirect2GDowngradeAnalyzer {} // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("Connection Release/Redirected Carrier 2G Downgrade") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.") } diff --git a/lib/src/analysis/imsi_requested.rs b/lib/src/analysis/imsi_requested.rs index 038510b..da6929a 100644 --- a/lib/src/analysis/imsi_requested.rs +++ b/lib/src/analysis/imsi_requested.rs @@ -98,11 +98,11 @@ impl ImsiRequestedAnalyzer { } impl Analyzer for ImsiRequestedAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("Identity (IMSI or IMEI) requested in suspicious manner") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from( "Tests whether the ME sends an Identity Request NAS message without either an associated attach request or auth accept message", ) diff --git a/lib/src/analysis/incomplete_sib.rs b/lib/src/analysis/incomplete_sib.rs index 612f308..3ef8763 100644 --- a/lib/src/analysis/incomplete_sib.rs +++ b/lib/src/analysis/incomplete_sib.rs @@ -24,11 +24,11 @@ impl IncompleteSibAnalyzer { } impl Analyzer for IncompleteSibAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("Incomplete SIB") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from("Tests whether a SIB1 message contains a full chain of followup sibs") } diff --git a/lib/src/analysis/nas_null_cipher.rs b/lib/src/analysis/nas_null_cipher.rs index 1e56806..fc4c9ed 100644 --- a/lib/src/analysis/nas_null_cipher.rs +++ b/lib/src/analysis/nas_null_cipher.rs @@ -24,11 +24,11 @@ impl NasNullCipherAnalyzer { } impl Analyzer for NasNullCipherAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("NAS Null Cipher Requested") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from( "Tests whether the MME requests to use a null cipher in the NAS security mode command", ) @@ -48,18 +48,18 @@ impl Analyzer for NasNullCipherAnalyzer { _ => return None, }; - if let NASMessage::EMMMessage(EMMMessage::EMMSecurityModeCommand(req)) = payload { - if req.nas_sec_algo.inner.ciph_algo == EPSEncryptionAlgorithmEEA0Null { - return Some(Event { - event_type: EventType::QualitativeWarning { - severity: Severity::High, - }, - message: format!( - "NAS Security mode command requested null cipher(packet {})", - self.packet_num - ), - }); - } + if let NASMessage::EMMMessage(EMMMessage::EMMSecurityModeCommand(req)) = payload + && req.nas_sec_algo.inner.ciph_algo == EPSEncryptionAlgorithmEEA0Null + { + return Some(Event { + event_type: EventType::QualitativeWarning { + severity: Severity::High, + }, + message: format!( + "NAS Security mode command requested null cipher(packet {})", + self.packet_num + ), + }); } None } diff --git a/lib/src/analysis/null_cipher.rs b/lib/src/analysis/null_cipher.rs index 22642fc..f15a79b 100644 --- a/lib/src/analysis/null_cipher.rs +++ b/lib/src/analysis/null_cipher.rs @@ -37,10 +37,10 @@ impl NullCipherAnalyzer { Some(&rat.security_algorithm_config) } }; - if let Some(security_config) = maybe_security_config { - if security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 { - return true; - } + if let Some(security_config) = maybe_security_config + && security_config.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 + { + return true; } } // Use map/flatten to dig into a long chain of nested Option types @@ -62,10 +62,10 @@ impl NullCipherAnalyzer { .as_ref() .and_then(|scg| scg.mobility_control_info_scg_r12.as_ref()) .and_then(|mci| mci.ciphering_algorithm_scg_r12.as_ref()); - if let Some(cipher) = maybe_cipher { - if cipher.0 == CipheringAlgorithm_r12::EEA0 { - return true; - } + if let Some(cipher) = maybe_cipher + && cipher.0 == CipheringAlgorithm_r12::EEA0 + { + return true; } } @@ -90,10 +90,10 @@ impl NullCipherAnalyzer { Some(&to_5gc.security_algorithm_config_r15) } }; - if let Some(security_algorithm) = maybe_security_algorithm { - if security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 { - return true; - } + if let Some(security_algorithm) = maybe_security_algorithm + && security_algorithm.ciphering_algorithm.0 == CipheringAlgorithm_r12::EEA0 + { + return true; } false } @@ -119,11 +119,11 @@ impl NullCipherAnalyzer { } impl Analyzer for NullCipherAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("Null Cipher") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from("Tests whether the cell suggests using a null cipher (EEA0)") } diff --git a/lib/src/analysis/priority_2g_downgrade.rs b/lib/src/analysis/priority_2g_downgrade.rs index 64a9150..eb78fcb 100644 --- a/lib/src/analysis/priority_2g_downgrade.rs +++ b/lib/src/analysis/priority_2g_downgrade.rs @@ -16,19 +16,15 @@ impl LteSib6And7DowngradeAnalyzer { &self, ie: &'a InformationElement, ) -> Option<&'a SystemInformation_r8_IEsSib_TypeAndInfo> { - if let InformationElement::LTE(lte_ie) = ie { - if let LteInformationElement::BcchDlSch(bcch_dl_sch_message) = &**lte_ie { - if let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation( - system_information, - )) = &bcch_dl_sch_message.message - { - if let SystemInformationCriticalExtensions::SystemInformation_r8(sib) = - &system_information.critical_extensions - { - return Some(&sib.sib_type_and_info); - } - } - } + if let InformationElement::LTE(lte_ie) = ie + && let LteInformationElement::BcchDlSch(bcch_dl_sch_message) = &**lte_ie + && let BCCH_DL_SCH_MessageType::C1(BCCH_DL_SCH_MessageType_c1::SystemInformation( + system_information, + )) = &bcch_dl_sch_message.message + && let SystemInformationCriticalExtensions::SystemInformation_r8(sib) = + &system_information.critical_extensions + { + return Some(&sib.sib_type_and_info); } None } @@ -36,11 +32,11 @@ impl LteSib6And7DowngradeAnalyzer { // TODO: keep track of SIB state to compare LTE reselection blocks w/ 2g/3g ones impl Analyzer for LteSib6And7DowngradeAnalyzer { - fn get_name(&self) -> Cow { + fn get_name(&self) -> Cow<'_, str> { Cow::from("LTE SIB 6/7 Downgrade") } - fn get_description(&self) -> Cow { + fn get_description(&self) -> Cow<'_, str> { Cow::from( "Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.", ) @@ -62,13 +58,16 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer { for carrier_info in &carrier_info_list.0 { if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority + && p == 0 { - if p == 0 { - return Some(Event { - event_type: EventType::QualitativeWarning { severity: Severity::High }, - message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), - }); - } + return Some(Event { + event_type: EventType::QualitativeWarning { + severity: Severity::High, + }, + message: + "LTE cell advertised a 3G cell for priority 0 reselection" + .to_string(), + }); } } } @@ -76,13 +75,16 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer { for carrier_info in &carrier_info_list.0 { if let Some(CellReselectionPriority(p)) = carrier_info.cell_reselection_priority + && p == 0 { - if p == 0 { - return Some(Event { - event_type: EventType::QualitativeWarning { severity: Severity::High }, - message: "LTE cell advertised a 3G cell for priority 0 reselection".to_string(), - }); - } + return Some(Event { + event_type: EventType::QualitativeWarning { + severity: Severity::High, + }, + message: + "LTE cell advertised a 3G cell for priority 0 reselection" + .to_string(), + }); } } } @@ -96,17 +98,15 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer { for carrier_info in &carrier_info_list.0 { if let Some(CellReselectionPriority(p)) = carrier_info.common_info.cell_reselection_priority + && p == 0 { - if p == 0 { - return Some(Event { - event_type: EventType::QualitativeWarning { - severity: Severity::High, - }, - message: - "LTE cell advertised a 2G cell for priority 0 reselection" - .to_string(), - }); - } + return Some(Event { + event_type: EventType::QualitativeWarning { + severity: Severity::High, + }, + message: "LTE cell advertised a 2G cell for priority 0 reselection" + .to_string(), + }); } } } diff --git a/lib/src/diag_device.rs b/lib/src/diag_device.rs index dd4c5cf..47aa496 100644 --- a/lib/src/diag_device.rs +++ b/lib/src/diag_device.rs @@ -198,10 +198,10 @@ impl DiagDevice { return Err(DiagDeviceError::DeviceWriteFailed(err)); } } - if let Err(err) = self.file.flush().await { - if err.kind() != ErrorKind::WriteZero { - return Err(DiagDeviceError::DeviceWriteFailed(err)); - } + if let Err(err) = self.file.flush().await + && err.kind() != ErrorKind::WriteZero + { + return Err(DiagDeviceError::DeviceWriteFailed(err)); } Ok(()) } diff --git a/lib/src/qmdl.rs b/lib/src/qmdl.rs index 386dee2..9a2f0c6 100644 --- a/lib/src/qmdl.rs +++ b/lib/src/qmdl.rs @@ -77,16 +77,16 @@ where pub async fn get_next_messages_container( &mut self, ) -> Result, std::io::Error> { - if let Some(max_bytes) = self.max_bytes { - if self.bytes_read >= max_bytes { - if self.bytes_read > max_bytes { - error!( - "warning: {} bytes read, but max_bytes was {}", - self.bytes_read, max_bytes - ); - } - return Ok(None); + if let Some(max_bytes) = self.max_bytes + && self.bytes_read >= max_bytes + { + if self.bytes_read > max_bytes { + error!( + "warning: {} bytes read, but max_bytes was {}", + self.bytes_read, max_bytes + ); } + return Ok(None); } let mut buf = Vec::new(); From 85b50bc3010da7aa7ee1641295ca0a835d012bec Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 8 Aug 2025 03:05:09 +0200 Subject: [PATCH 2/3] Remove unpack! macro --- .../analysis/connection_redirect_downgrade.rs | 44 +++++++++---------- lib/src/analysis/incomplete_sib.rs | 14 +++--- lib/src/analysis/util.rs | 32 -------------- 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/lib/src/analysis/connection_redirect_downgrade.rs b/lib/src/analysis/connection_redirect_downgrade.rs index d56228b..872b0bd 100644 --- a/lib/src/analysis/connection_redirect_downgrade.rs +++ b/lib/src/analysis/connection_redirect_downgrade.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use super::analyzer::{Analyzer, Event, EventType, Severity}; use super::information_element::{InformationElement, LteInformationElement}; -use super::util::unpack; use telcom_parser::lte_rrc::{ DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions, RRCConnectionReleaseCriticalExtensions_c1, RedirectedCarrierInfo, @@ -27,27 +26,28 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer { } fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { - unpack!(InformationElement::LTE(lte_ie) = ie); - let message = match &**lte_ie { - LteInformationElement::DlDcch(msg_cont) => &msg_cont.message, - _ => return None, - }; - unpack!(DL_DCCH_MessageType::C1(c1) = message); - unpack!(DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1); - unpack!(RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions); - unpack!(RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1); - unpack!(Some(carrier_info) = &r8_ies.redirected_carrier_info); - match carrier_info { - RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event { - event_type: EventType::QualitativeWarning { - severity: Severity::High, - }, - message: "Detected 2G downgrade".to_owned(), - }), - _ => Some(Event { - event_type: EventType::Informational, - message: format!("RRCConnectionRelease CarrierInfo: {carrier_info:?}"), - }), + if let InformationElement::LTE(lte_ie) = ie + && let LteInformationElement::DlDcch(msg_cont) = &**lte_ie + && let DL_DCCH_MessageType::C1(c1) = &msg_cont.message + && let DL_DCCH_MessageType_c1::RrcConnectionRelease(release) = c1 + && let RRCConnectionReleaseCriticalExtensions::C1(c1) = &release.critical_extensions + && let RRCConnectionReleaseCriticalExtensions_c1::RrcConnectionRelease_r8(r8_ies) = c1 + && let Some(carrier_info) = &r8_ies.redirected_carrier_info + { + match carrier_info { + RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event { + event_type: EventType::QualitativeWarning { + severity: Severity::High, + }, + message: "Detected 2G downgrade".to_owned(), + }), + _ => Some(Event { + event_type: EventType::Informational, + message: format!("RRCConnectionRelease CarrierInfo: {carrier_info:?}"), + }), + } + } else { + None } } } diff --git a/lib/src/analysis/incomplete_sib.rs b/lib/src/analysis/incomplete_sib.rs index 3ef8763..e1abb7a 100644 --- a/lib/src/analysis/incomplete_sib.rs +++ b/lib/src/analysis/incomplete_sib.rs @@ -2,8 +2,6 @@ use std::borrow::Cow; use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1}; -use crate::analysis::util::unpack; - use super::analyzer::{Analyzer, Event, EventType, Severity}; use super::information_element::{InformationElement, LteInformationElement}; @@ -39,12 +37,12 @@ impl Analyzer for IncompleteSibAnalyzer { fn analyze_information_element(&mut self, ie: &InformationElement) -> Option { self.packet_num += 1; - unpack!(InformationElement::LTE(lte_ie) = ie); - unpack!(LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie); - unpack!(BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message); - unpack!(BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1); - - if sib1.scheduling_info_list.0.len() < 2 { + if let InformationElement::LTE(lte_ie) = ie + && let LteInformationElement::BcchDlSch(sch_msg) = &**lte_ie + && let BCCH_DL_SCH_MessageType::C1(c1) = &sch_msg.message + && let BCCH_DL_SCH_MessageType_c1::SystemInformationBlockType1(sib1) = c1 + && sib1.scheduling_info_list.0.len() < 2 + { return Some(Event { event_type: EventType::QualitativeWarning { severity: Severity::Medium, diff --git a/lib/src/analysis/util.rs b/lib/src/analysis/util.rs index ac128e4..8b13789 100644 --- a/lib/src/analysis/util.rs +++ b/lib/src/analysis/util.rs @@ -1,33 +1 @@ -// Unpacks a pattern, or returns None. -// -// # Examples -// You can use `unpack!` to unroll highly nested enums like this: -// ``` -// enum Foo { -// A(Bar), -// B, -// } -// -// enum Bar { -// C(Baz) -// } -// -// struct Baz; -// -// fn get_bang(foo: Foo) -> Option { -// unpack!(Foo::A(bar) = foo); -// unpack!(Bar::C(baz) = bar); -// baz -// } -// ``` -// -macro_rules! unpack { - ($pat:pat = $val:expr) => { - let $pat = $val else { - return None; - }; - }; -} -// this is apparently how you make a macro publicly usable from this module -pub(crate) use unpack; From ce599dc4329b691cc82528afbd56225594be137c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Fri, 8 Aug 2025 18:02:40 +0200 Subject: [PATCH 3/3] specify package.rust-version for daemon --- daemon/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index c649da6..c8fb6cb 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -2,6 +2,7 @@ name = "rayhunter-daemon" version = "0.5.1" edition = "2024" +rust-version = "1.88.0" [dependencies] rayhunter = { path = "../lib" }