Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c18e0f7b authored by Abhishek Pandit-Subedi's avatar Abhishek Pandit-Subedi
Browse files

floss: Add rules engine + connections rule

Add a rules engine that can be run on a log file. Seed this with
a connections rule group that monitors connections and reports
abnormalities.

Bug: 262928525
Tag: #floss
Test: ./build.py --target utils (and run binary)
Change-Id: Id32ac2763ef19762a968437c4b8dfcb55ee13fdc
parent 79b17b9d
Loading
Loading
Loading
Loading
+74 −0
Original line number Diff line number Diff line
//! Handles stream processing of commands and events.

use std::collections::HashMap;
use std::io::Write;

use crate::parser::Packet;

/// Trait that describes a single rule processor. A rule should be used to represent a certain type
/// of analysis (for example: ACL Connections rule may keep track of all ACL connections and report
/// on failed connections).
pub trait Rule {
    /// Process a single packet.
    fn process(&mut self, packet: &Packet);

    /// Generate a report for this rule based on the input stream so far. Usually, this should
    /// report on the instances of this rule that were discovered or any error conditions that are
    /// relevant to this rule.
    fn report(&self, writer: &mut dyn Write);
}

/// Grouping of rules. This is used to make it easier to enable/disable certain rules for
/// processing a file.
pub struct RuleGroup {
    rules: Vec<Box<dyn Rule>>,
}

impl RuleGroup {
    pub fn new() -> Self {
        RuleGroup { rules: vec![] }
    }

    pub fn add_rule(&mut self, rule: Box<dyn Rule>) {
        self.rules.push(rule);
    }

    pub fn process(&mut self, packet: &Packet) {
        for rule in &mut self.rules {
            rule.process(packet);
        }
    }

    pub fn report(&mut self, writer: &mut dyn Write) {
        for rule in &self.rules {
            rule.report(writer);
        }
    }
}
/// Main entry point to process input data and run rules on them.
pub struct RuleEngine {
    groups: HashMap<String, RuleGroup>,
}

impl RuleEngine {
    pub fn new() -> Self {
        RuleEngine { groups: HashMap::new() }
    }

    pub fn add_rule_group(&mut self, name: String, group: RuleGroup) {
        self.groups.insert(name, group);
    }

    /// Consume a packet and run it through the various rules processors.
    pub fn process(&mut self, packet: Packet) {
        for group in self.groups.values_mut() {
            group.process(&packet);
        }
    }

    pub fn report(&mut self, writer: &mut dyn Write) {
        for group in self.groups.values_mut() {
            group.report(writer);
        }
    }
}
+420 −0
Original line number Diff line number Diff line
///! Rule group for tracking connection related issues.
use chrono::NaiveDateTime;
use std::collections::HashMap;
use std::io::Write;

use crate::engine::{Rule, RuleGroup};
use crate::parser::{Packet, PacketChild};
use bt_packets::custom_types::Address;
use bt_packets::hci::{
    AclCommandChild, CommandChild, CommandStatusPacket, ConnectionManagementCommandChild,
    ErrorCode, EventChild, EventPacket, LeConnectionManagementCommandChild, LeMetaEventChild,
    OpCode, ScoConnectionCommandChild, SubeventCode,
};

/// Valid values are in the range 0x0000-0x0EFF.
pub type ConnectionHandle = u16;

/// Arbitrary invalid connection handle.
pub const INVALID_CONN_HANDLE: u16 = 0xfffeu16;

/// When we attempt to create a sco connection on an unknown handle, use this address as
/// a placeholder.
pub const UNKNOWN_SCO_ADDRESS: Address = Address { bytes: [0xde, 0xad, 0xbe, 0xef, 0x00, 0x00] };

/// Keeps track of connections and identifies odd disconnections.
struct OddDisconnectionsRule {
    /// Timestamp on first packet in current log.
    start_of_log: Option<NaiveDateTime>,

    /// Handles that had successful complete connections. The value has the timestamp of the
    /// connection completion and the address of the device.
    active_handles: HashMap<ConnectionHandle, (NaiveDateTime, Address)>,

    connection_attempt: HashMap<Address, Packet>,
    last_connection_attempt: Option<Address>,

    le_connection_attempt: HashMap<Address, Packet>,
    last_le_connection_attempt: Option<Address>,

    sco_connection_attempt: HashMap<Address, Packet>,
    last_sco_connection_attempt: Option<Address>,

    /// Interesting occurrences surfaced by this rule.
    reportable: Vec<(NaiveDateTime, String)>,
}

impl OddDisconnectionsRule {
    pub fn new() -> Self {
        OddDisconnectionsRule {
            start_of_log: None,
            active_handles: HashMap::new(),
            connection_attempt: HashMap::new(),
            last_connection_attempt: None,
            le_connection_attempt: HashMap::new(),
            last_le_connection_attempt: None,
            sco_connection_attempt: HashMap::new(),
            last_sco_connection_attempt: None,
            reportable: vec![],
        }
    }

    pub fn process_classic_connection(
        &mut self,
        conn: &ConnectionManagementCommandChild,
        packet: &Packet,
    ) {
        let has_existing = match conn {
            ConnectionManagementCommandChild::CreateConnection(cc) => {
                self.last_connection_attempt = Some(cc.get_bd_addr());
                self.connection_attempt.insert(cc.get_bd_addr(), packet.clone())
            }

            ConnectionManagementCommandChild::AcceptConnectionRequest(ac) => {
                self.last_connection_attempt = Some(ac.get_bd_addr());
                self.connection_attempt.insert(ac.get_bd_addr(), packet.clone())
            }

            _ => None,
        };

        if let Some(p) = has_existing {
            self.reportable.push((
                p.ts,
                format!("Dangling connection attempt at {:?} replaced with {:?}", p, packet),
            ));
        }
    }

    pub fn process_sco_connection(
        &mut self,
        sco_conn: &ScoConnectionCommandChild,
        packet: &Packet,
    ) {
        let handle = match sco_conn {
            ScoConnectionCommandChild::SetupSynchronousConnection(ssc) => {
                ssc.get_connection_handle()
            }

            ScoConnectionCommandChild::EnhancedSetupSynchronousConnection(essc) => {
                essc.get_connection_handle()
            }

            _ => INVALID_CONN_HANDLE,
        };

        let address = match self.active_handles.get(&handle).as_ref() {
            Some((_ts, address)) => address,
            None => &UNKNOWN_SCO_ADDRESS,
        };

        let has_existing = match sco_conn {
            ScoConnectionCommandChild::SetupSynchronousConnection(_)
            | ScoConnectionCommandChild::EnhancedSetupSynchronousConnection(_) => {
                self.last_sco_connection_attempt = Some(address.clone());
                self.sco_connection_attempt.insert(address.clone(), packet.clone())
            }

            ScoConnectionCommandChild::AcceptSynchronousConnection(asc) => {
                self.last_sco_connection_attempt = Some(asc.get_bd_addr());
                self.sco_connection_attempt.insert(asc.get_bd_addr(), packet.clone())
            }

            ScoConnectionCommandChild::EnhancedAcceptSynchronousConnection(easc) => {
                self.last_sco_connection_attempt = Some(easc.get_bd_addr());
                self.sco_connection_attempt.insert(easc.get_bd_addr(), packet.clone())
            }

            _ => None,
        };

        if let Some(p) = has_existing {
            self.reportable.push((
                p.ts,
                format!("Dangling sco connection attempt at {:?} replaced with {:?}", p, packet),
            ));
        }
    }

    pub fn process_le_conn_connection(
        &mut self,
        le_conn: &LeConnectionManagementCommandChild,
        packet: &Packet,
    ) {
        let has_existing = match le_conn {
            LeConnectionManagementCommandChild::LeCreateConnection(create) => {
                self.last_le_connection_attempt = Some(create.get_peer_address());
                self.le_connection_attempt.insert(create.get_peer_address().clone(), packet.clone())
            }

            LeConnectionManagementCommandChild::LeExtendedCreateConnection(extcreate) => {
                self.last_le_connection_attempt = Some(extcreate.get_peer_address());
                self.le_connection_attempt
                    .insert(extcreate.get_peer_address().clone(), packet.clone())
            }

            _ => None,
        };

        if let Some(p) = has_existing {
            self.reportable.push((
                p.ts,
                format!("Dangling LE connection attempt at {:?} replaced with {:?}", p, packet),
            ));
        }
    }

    pub fn process_command_status(&mut self, cs: &CommandStatusPacket, packet: &Packet) {
        // Clear last connection attempt since it was successful.
        let last_address = match cs.get_command_op_code() {
            OpCode::CreateConnection | OpCode::AcceptConnectionRequest => {
                self.last_connection_attempt.take()
            }

            OpCode::SetupSynchronousConnection
            | OpCode::AcceptSynchronousConnection
            | OpCode::EnhancedSetupSynchronousConnection
            | OpCode::EnhancedAcceptSynchronousConnection => {
                self.last_sco_connection_attempt.take()
            }

            OpCode::LeCreateConnection | OpCode::LeExtendedCreateConnection => {
                self.last_le_connection_attempt.take()
            }

            _ => None,
        };

        if let Some(address) = last_address {
            if cs.get_status() != ErrorCode::Success {
                self.reportable.push((
                    packet.ts,
                    format!("Failing command status on [{:?}]: {:?}", address, cs),
                ));

                // Also remove the connection attempt.
                match cs.get_command_op_code() {
                    OpCode::CreateConnection | OpCode::AcceptConnectionRequest => {
                        self.connection_attempt.remove(&address);
                    }

                    OpCode::SetupSynchronousConnection
                    | OpCode::AcceptSynchronousConnection
                    | OpCode::EnhancedSetupSynchronousConnection
                    | OpCode::EnhancedAcceptSynchronousConnection => {
                        self.sco_connection_attempt.remove(&address);
                    }

                    OpCode::LeCreateConnection => {
                        self.le_connection_attempt.remove(&address);
                    }

                    _ => (),
                }
            }
        } else {
            if cs.get_status() != ErrorCode::Success {
                self.reportable.push((
                    packet.ts,
                    format!("Failing command status on unknown address: {:?}", cs),
                ));
            }
        }
    }

    pub fn process_event(&mut self, ev: &EventPacket, packet: &Packet) {
        match ev.specialize() {
            EventChild::ConnectionComplete(cc) => {
                match self.connection_attempt.remove(&cc.get_bd_addr()) {
                    Some(_) => {
                        if cc.get_status() == ErrorCode::Success {
                            self.active_handles
                                .insert(cc.get_connection_handle(), (packet.ts, cc.get_bd_addr()));
                        } else {
                            self.reportable.push((
                                packet.ts,
                                format!(
                                    "ConnectionComplete error {:?} for addr {:?} (handle={})",
                                    cc.get_status(),
                                    cc.get_bd_addr(),
                                    cc.get_connection_handle()
                                ),
                            ));
                        }
                    }
                    None => {
                        self.reportable.push((
                            packet.ts,
                            format!(
                            "ConnectionComplete with status {:?} for unknown addr {:?} (handle={})",
                            cc.get_status(),
                            cc.get_bd_addr(),
                            cc.get_connection_handle()
                        ),
                        ));
                    }
                }
            }

            EventChild::DisconnectionComplete(dsc) => {
                if self.active_handles.remove(&dsc.get_connection_handle()).is_none() {
                    self.reportable.push((
                        packet.ts,
                        format!(
                            "DisconnectionComplete for unknown handle {} with status={:?}",
                            dsc.get_connection_handle(),
                            dsc.get_status()
                        ),
                    ));
                }
            }

            EventChild::SynchronousConnectionComplete(scc) => {
                match self.sco_connection_attempt.remove(&scc.get_bd_addr()) {
                    Some(_) => {
                        if scc.get_status() == ErrorCode::Success {
                            self.active_handles.insert(
                                scc.get_connection_handle(),
                                (packet.ts, scc.get_bd_addr()),
                            );
                        } else {
                            self.reportable.push((
                                packet.ts,
                                format!(
                                    "SynchronousConnectionComplete error {:?} for addr {:?} (handle={})",
                                    scc.get_status(),
                                    scc.get_bd_addr(),
                                    scc.get_connection_handle()
                                ),
                            ));
                        }
                    }
                    None => {
                        self.reportable.push((
                            packet.ts,
                            format!(
                            "SynchronousConnectionComplete with status {:?} for unknown addr {:?} (handle={})",
                            scc.get_status(),
                            scc.get_bd_addr(),
                            scc.get_connection_handle()
                        ),
                        ));
                    }
                }
            }

            EventChild::LeMetaEvent(lme) => {
                let details = match lme.specialize() {
                    LeMetaEventChild::LeConnectionComplete(lcc) => Some((
                        lcc.get_status(),
                        lcc.get_connection_handle(),
                        lcc.get_peer_address(),
                    )),
                    LeMetaEventChild::LeEnhancedConnectionComplete(lecc) => Some((
                        lecc.get_status(),
                        lecc.get_connection_handle(),
                        lecc.get_peer_address(),
                    )),
                    _ => None,
                };

                if let Some((status, handle, address)) = details {
                    match self.le_connection_attempt.remove(&address) {
                        Some(_) => {
                            if status == ErrorCode::Success {
                                self.active_handles.insert(handle, (packet.ts, address));
                            } else {
                                self.reportable.push((
                                    packet.ts,
                                    format!(
                                        "LeConnectionComplete error {:?} for addr {:?} (handle={})",
                                        status, address, handle
                                    ),
                                ));
                            }
                        }
                        None => {
                            self.reportable.push((packet.ts, format!("LeConnectionComplete with status {:?} for unknown addr {:?} (handle={})", status, address, handle)));
                        }
                    }
                }
            }

            _ => (),
        }
    }
}

impl Rule for OddDisconnectionsRule {
    fn process(&mut self, packet: &Packet) {
        if self.start_of_log.is_none() {
            self.start_of_log = Some(packet.ts.clone());
        }

        match &packet.inner {
            PacketChild::HciCommand(cmd) => match cmd.specialize() {
                CommandChild::AclCommand(aclpkt) => match aclpkt.specialize() {
                    AclCommandChild::ConnectionManagementCommand(conn) => {
                        self.process_classic_connection(&conn.specialize(), packet)
                    }
                    AclCommandChild::ScoConnectionCommand(sco_conn) => {
                        self.process_sco_connection(&sco_conn.specialize(), packet)
                    }
                    AclCommandChild::LeConnectionManagementCommand(le_conn) => {
                        self.process_le_conn_connection(&le_conn.specialize(), packet)
                    }
                    _ => (),
                },
                _ => (),
            },

            PacketChild::HciEvent(ev) => match ev.specialize() {
                EventChild::CommandStatus(cs) => match cs.get_command_op_code() {
                    OpCode::CreateConnection
                    | OpCode::AcceptConnectionRequest
                    | OpCode::SetupSynchronousConnection
                    | OpCode::AcceptSynchronousConnection
                    | OpCode::EnhancedSetupSynchronousConnection
                    | OpCode::EnhancedAcceptSynchronousConnection
                    | OpCode::LeCreateConnection
                    | OpCode::LeExtendedCreateConnection => {
                        self.process_command_status(&cs, packet);
                    }
                    _ => (),
                },

                EventChild::ConnectionComplete(_)
                | EventChild::DisconnectionComplete(_)
                | EventChild::SynchronousConnectionComplete(_) => {
                    self.process_event(&ev, packet);
                }

                EventChild::LeMetaEvent(lme) => match lme.get_subevent_code() {
                    SubeventCode::ConnectionComplete | SubeventCode::EnhancedConnectionComplete => {
                        self.process_event(&ev, packet);
                    }
                    _ => (),
                },

                _ => (),
            },
        }
    }

    fn report(&self, writer: &mut dyn Write) {
        if self.reportable.len() > 0 {
            let _ = writeln!(writer, "OddDisconnectionsRule report:");
            for (ts, message) in self.reportable.iter() {
                let _ = writeln!(writer, "[{:?}] {}", ts, message);
            }
        }
    }
}

/// Get a rule group with connection rules.
pub fn get_connections_group() -> RuleGroup {
    let mut group = RuleGroup::new();
    group.add_rule(Box::new(OddDisconnectionsRule::new()));

    group
}
+2 −0
Original line number Diff line number Diff line
///! Rule groups for hcidoc.
pub(crate) mod connections;
+19 −15
Original line number Diff line number Diff line
#[macro_use]
extern crate num_derive;

use clap::{Arg, Command};
use std::io::Write;

mod engine;
mod groups;
mod parser;

use crate::engine::RuleEngine;
use crate::groups::connections;
use crate::parser::{LinuxSnoopOpcodes, LogParser, LogType, Packet};
use bt_packets;
use clap::{Arg, Command};

fn main() {
    let matches = Command::new("hcidoc")
@@ -39,27 +44,26 @@ fn main() {
        }
    };

    let mut printed = 0usize;
    if let LogType::LinuxSnoop(header) = log_type {
        println!("Reading snoop file: {:?}", header);
        for (pos, v) in parser.get_snoop_iterator().expect("Not a linux snoop file").enumerate() {
            if printed > 50 {
                break;
            }
    // Create engine with default rule groups.
    let mut engine = RuleEngine::new();
    engine.add_rule_group("Connections".into(), connections::get_connections_group());

    // Decide where to write output.
    let mut writer: Box<dyn Write> = Box::new(std::io::stdout());

    if let LogType::LinuxSnoop(_header) = log_type {
        for (pos, v) in parser.get_snoop_iterator().expect("Not a linux snoop file").enumerate() {
            match Packet::try_from(&v) {
                Ok(p) => {
                    println!("#{}: {:?}", pos, p);
                    printed = printed + 1;
                }
                Ok(p) => engine.process(p),
                Err(e) => match v.opcode() {
                    LinuxSnoopOpcodes::CommandPacket | LinuxSnoopOpcodes::EventPacket => {
                        println!("#{}: {}", pos, e);
                        printed = printed + 1;
                        eprintln!("#{}: {}", pos, e);
                    }
                    _ => (),
                },
            }
        }

        engine.report(&mut writer);
    }
}
+10 −13
Original line number Diff line number Diff line
//! Parsing of various Bluetooth packets.
use chrono::NaiveDateTime;
use num_traits::cast::{FromPrimitive, ToPrimitive};
use num_traits::cast::FromPrimitive;
use std::convert::TryFrom;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Seek};
@@ -189,7 +189,7 @@ impl<'a> Iterator for LinuxSnoopReader<'a> {
                // |UnexpectedEof| could be seen since we're trying to read more
                // data than is available (i.e. end of file).
                if e.kind() != ErrorKind::UnexpectedEof {
                    println!("Error reading snoop file: {:?}", e);
                    eprintln!("Error reading snoop file: {:?}", e);
                }
                return None;
            }
@@ -203,7 +203,7 @@ impl<'a> Iterator for LinuxSnoopReader<'a> {
                    match self.fd.read(&mut rem_data[0..size]) {
                        Ok(b) => {
                            if b != size {
                                println!(
                                eprintln!(
                                    "Size({}) doesn't match bytes read({}). Aborting...",
                                    size, b
                                );
@@ -214,7 +214,7 @@ impl<'a> Iterator for LinuxSnoopReader<'a> {
                            Some(p)
                        }
                        Err(e) => {
                            println!("Couldn't read any packet data: {}", e);
                            eprintln!("Couldn't read any packet data: {}", e);
                            None
                        }
                    }
@@ -222,10 +222,7 @@ impl<'a> Iterator for LinuxSnoopReader<'a> {
                    Some(p)
                }
            }
            Err(e) => {
                println!("Failed to parse data: {:?}", e);
                None
            }
            Err(_) => None,
        }
    }
}
@@ -282,7 +279,7 @@ impl<'a> LogParser {
}

/// Data owned by a packet.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum PacketChild {
    HciCommand(CommandPacket),
    HciEvent(EventPacket),
@@ -310,16 +307,16 @@ impl<'a> TryFrom<&'a LinuxSnoopPacket> for PacketChild {
}

/// A single processable packet of data.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Packet {
    /// Timestamp of this packet
    ts: NaiveDateTime,
    pub ts: NaiveDateTime,

    /// Which adapter this packet is for. Unassociated packets should use 0xFFFE.
    adapter_index: u16,
    pub adapter_index: u16,

    /// Inner data for this packet.
    inner: PacketChild,
    pub inner: PacketChild,
}

impl<'a> TryFrom<&'a LinuxSnoopPacket> for Packet {