Merge branch 'main' into notifications

This commit is contained in:
Simon Fondrie-Teitler
2025-08-09 14:17:22 -04:00
15 changed files with 142 additions and 179 deletions

View File

@@ -2,7 +2,7 @@
name = "rayhunter-daemon"
version = "0.5.1"
edition = "2024"
rust-version = "1.88"
rust-version = "1.88.0"
[dependencies]
rayhunter = { path = "../lib" }

View File

@@ -96,16 +96,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}");

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<ADBUSBDevice> {
Ok::<(ADBUSBDevice, Vec<u8>), 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(());
}
}
}

View File

@@ -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<str>;
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<str>;
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

View File

@@ -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,
@@ -14,11 +13,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<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("Connection Release/Redirected Carrier 2G Downgrade")
}
fn get_description(&self) -> Cow<str> {
fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests if a cell releases our connection and redirects us to a 2G cell.")
}
@@ -27,27 +26,28 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
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
}
}
}

View File

@@ -98,11 +98,11 @@ impl ImsiRequestedAnalyzer {
}
impl Analyzer for ImsiRequestedAnalyzer {
fn get_name(&self) -> Cow<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("Identity (IMSI or IMEI) requested in suspicious manner")
}
fn get_description(&self) -> Cow<str> {
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",
)

View File

@@ -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};
@@ -24,11 +22,11 @@ impl IncompleteSibAnalyzer {
}
impl Analyzer for IncompleteSibAnalyzer {
fn get_name(&self) -> Cow<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("Incomplete SIB")
}
fn get_description(&self) -> Cow<str> {
fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests whether a SIB1 message contains a full chain of followup sibs")
}
@@ -39,12 +37,12 @@ impl Analyzer for IncompleteSibAnalyzer {
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event> {
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,

View File

@@ -24,11 +24,11 @@ impl NasNullCipherAnalyzer {
}
impl Analyzer for NasNullCipherAnalyzer {
fn get_name(&self) -> Cow<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("NAS Null Cipher Requested")
}
fn get_description(&self) -> Cow<str> {
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
}

View File

@@ -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<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("Null Cipher")
}
fn get_description(&self) -> Cow<str> {
fn get_description(&self) -> Cow<'_, str> {
Cow::from("Tests whether the cell suggests using a null cipher (EEA0)")
}

View File

@@ -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<str> {
fn get_name(&self) -> Cow<'_, str> {
Cow::from("LTE SIB 6/7 Downgrade")
}
fn get_description(&self) -> Cow<str> {
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(),
});
}
}
}

View File

@@ -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<Baz> {
// 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;

View File

@@ -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(())
}

View File

@@ -77,16 +77,16 @@ where
pub async fn get_next_messages_container(
&mut self,
) -> Result<Option<MessagesContainer>, 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();