use std::collections::VecDeque;
use tokio::sync::mpsc::Sender;
use tokio::task::JoinHandle;
use crate::capture::CaptureSource;
use crate::craft::CraftState;
use crate::net::inspector::CredentialHit;
use crate::net::security::SecurityEngine;
use crate::pcap_replay::ReplayState;
use crate::scan::ScanState;
use crate::sim::capture::SimulatedCapture;
use crate::sim::dynamic::DynEntry;
use crate::export::PcapWriter;
use crate::filter::PacketFilter;
use crate::net::flow::{FlowTracker, FlowSort, FlowKey};
use crate::net::lua_plugin::PluginManager;
use crate::net::packet::Packet;
use crate::dissector::DissectorDef;
use crate::net::packet::TreeSection;
use crate::tabs::Tab;
use crate::traceroute::TracerouteState;
const MAX_PACKETS: usize = 10_000;
#[derive(Debug, Clone, PartialEq)]
pub enum SecuritySubTab {
Ids,
Credentials,
OsFingerprint,
ArpWatch,
DnsTunnel,
HttpAnalytics,
TlsWeakness,
BruteForce,
VulnHits,
Replay,
}
fn list_interfaces() -> Vec<String> {
let mut ifaces = vec!["simulated".to_string()];
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
let mut sys: Vec<String> = entries
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
sys.sort();
ifaces.extend(sys);
}
ifaces
}
pub struct App {
pub active_tab: Tab,
pub packets: VecDeque<Packet>,
pub filtered: Vec<usize>,
pub selected: Option<usize>,
pub total_bytes: u64,
pub packet_counter: u64,
pub capturing: bool,
capture_handle: Option<JoinHandle<()>>,
packet_tx: Sender<Packet>,
pub picking_iface: bool,
pub iface_list: Vec<String>,
pub iface_sel: usize,
pub selected_iface: String,
pub filter: PacketFilter,
pub rate_history: Vec<u32>,
pub rate_this_sec: u32,
rate_tick: u32,
pub dyn_log: Vec<DynEntry>,
pub dyn_scroll: usize,
pub analysis_section: usize,
pub strings_filter: String,
pub _hex_scroll: u16,
pub recording: bool,
pub pcap_path: String,
pcap_writer: Option<PcapWriter>,
pub show_help: bool,
pub dissectors: Vec<DissectorDef>,
pub strings_search_active: bool,
pub strings_selected: Option<usize>,
pub strings_scroll: usize,
pub flow_tracker: FlowTracker,
pub flows_selected: Option<usize>,
pub flows_sort: FlowSort,
pub stream_overlay: Option<(String, Vec<(bool, Vec<u8>)>)>,
pub lua_plugins: PluginManager,
pub lua_reload_msg: Option<String>,
pub craft: CraftState,
pub traceroute: TracerouteState,
pub security: SecurityEngine,
pub credentials: Vec<CredentialHit>,
pub scan: ScanState,
pub replay: ReplayState,
pub security_tab: SecuritySubTab,
pub security_scroll: usize,
pub scanner_scroll: usize,
pub replay_editing: bool,
pub scan_editing: bool,
}
impl App {
pub fn new(packet_tx: Sender<Packet>) -> Self {
let iface_list = list_interfaces();
Self {
active_tab: Tab::Packets,
packets: VecDeque::new(),
filtered: Vec::new(),
selected: None,
total_bytes: 0,
packet_counter: 0,
capturing: false,
capture_handle: None,
packet_tx,
picking_iface: true,
iface_list,
iface_sel: 0,
selected_iface: "simulated".to_string(),
filter: PacketFilter::default(),
rate_history: vec![0u32; 60],
rate_this_sec: 0,
rate_tick: 0,
dyn_log: Vec::new(),
dyn_scroll: 0,
analysis_section: 0,
strings_filter: String::new(),
_hex_scroll: 0,
recording: false,
pcap_path: String::new(),
pcap_writer: None,
show_help: false,
dissectors: crate::dissector::load(),
strings_search_active: false,
strings_selected: None,
strings_scroll: 0,
flow_tracker: FlowTracker::new(),
flows_selected: None,
flows_sort: FlowSort::Bytes,
stream_overlay: None,
lua_plugins: {
let mut pm = PluginManager::new();
pm.reload();
pm
},
lua_reload_msg: None,
craft: CraftState::default(),
traceroute: TracerouteState::default(),
security: SecurityEngine::default(),
credentials: Vec::new(),
scan: ScanState::new(),
replay: ReplayState::default(),
security_tab: SecuritySubTab::Ids,
security_scroll: 0,
scanner_scroll: 0,
replay_editing: false,
scan_editing: false,
}
}
pub fn reload_lua_plugins(&mut self) {
self.lua_plugins.reload();
let n = self.lua_plugins.plugin_count();
let p = self.lua_plugins.proto_count();
let errs = self.lua_plugins.error_log.len();
if errs > 0 {
self.lua_reload_msg = Some(format!(
"Lua: {n} files, {p} dissectors — {} error(s)", errs
));
} else {
self.lua_reload_msg = Some(format!(
"Lua: {n} files, {p} dissectors loaded"
));
}
}
pub fn strings_move_down(&mut self, list_len: usize) {
if self.capturing || list_len == 0 { return; }
let cur = self.strings_selected.unwrap_or(0);
let next = (cur + 1).min(list_len.saturating_sub(1));
self.strings_selected = Some(next);
if next > self.strings_scroll { self.strings_scroll = next; }
}
pub fn strings_move_up(&mut self) {
if self.capturing { return; }
let cur = self.strings_selected.unwrap_or(0);
let prev = cur.saturating_sub(1);
self.strings_selected = Some(prev);
if prev < self.strings_scroll { self.strings_scroll = prev; }
}
pub fn strings_select(&mut self) {
if self.strings_selected.is_none() {
self.strings_selected = Some(0);
}
}
pub fn strings_deselect(&mut self) {
self.strings_selected = None;
}
pub fn strings_list_len(&self) -> usize {
const MIN_LEN: usize = 4;
let mut count = 0usize;
for pkt in self.packets.iter().take(500) {
let mut in_run = false;
let mut run_start = 0usize;
for (i, &b) in pkt.bytes.iter().enumerate() {
if b >= 32 && b < 127 {
if !in_run { run_start = i; in_run = true; }
} else if in_run {
in_run = false;
if i - run_start >= MIN_LEN { count += 1; }
}
}
if in_run && pkt.bytes.len() - run_start >= MIN_LEN { count += 1; }
}
if self.strings_filter.is_empty() {
count
} else {
let q = self.strings_filter.to_lowercase();
let mut filt_count = 0usize;
for pkt in self.packets.iter().take(500) {
let bytes = &pkt.bytes;
let mut in_run = false;
let mut run_start = 0usize;
for (i, &b) in bytes.iter().enumerate() {
if b >= 32 && b < 127 {
if !in_run { run_start = i; in_run = true; }
} else if in_run {
in_run = false;
if i - run_start >= MIN_LEN {
let val = String::from_utf8_lossy(&bytes[run_start..i]);
if val.to_lowercase().contains(&q) { filt_count += 1; }
}
}
}
if in_run && pkt.bytes.len() - run_start >= MIN_LEN {
let val = String::from_utf8_lossy(&pkt.bytes[run_start..]);
if val.to_lowercase().contains(&q) { filt_count += 1; }
}
}
filt_count
}
}
pub fn packet_by_no(&self, no: u64) -> Option<&Packet> {
self.packets.iter().find(|p| p.no == no)
}
pub fn dissect_packet(&self, pkt: &Packet) -> Vec<TreeSection> {
let mut sections = crate::net::tree::build_tree(pkt);
crate::dissector::apply(&self.dissectors, pkt, &mut sections);
self.lua_plugins.apply(pkt, &mut sections);
sections
}
pub fn iface_down(&mut self) {
if self.iface_sel + 1 < self.iface_list.len() { self.iface_sel += 1; }
}
pub fn iface_up(&mut self) { self.iface_sel = self.iface_sel.saturating_sub(1); }
pub fn confirm_iface(&mut self) {
self.selected_iface = self.iface_list[self.iface_sel].clone();
self.picking_iface = false;
self.abort_capture();
if self.selected_iface == "simulated" {
self.capture_handle = Some(SimulatedCapture.run(self.packet_tx.clone()));
self.capturing = true;
} else {
#[cfg(feature = "real-capture")]
{
use crate::capture::live::LiveCapture;
let source = LiveCapture { iface: self.selected_iface.clone(), filter: None };
self.capture_handle = Some(source.run(self.packet_tx.clone()));
self.capturing = true;
}
#[cfg(not(feature = "real-capture"))]
{ self.capturing = false; }
}
}
pub fn switch_interface(&mut self) {
self.abort_capture();
self.picking_iface = true;
self.capturing = false;
}
fn abort_capture(&mut self) {
if let Some(handle) = self.capture_handle.take() { handle.abort(); }
}
pub fn ingest_packet(&mut self, pkt: Packet) {
if !self.capturing { return; }
self.ingest_packet_inner(pkt);
}
pub fn inject_packet(&mut self, pkt: Packet) {
self.ingest_packet_inner(pkt);
}
fn ingest_packet_inner(&mut self, pkt: Packet) {
self.packet_counter += 1;
self.total_bytes += pkt.length as u64;
self.rate_this_sec += 1;
self.flow_tracker.update(&pkt);
self.security.update(&pkt);
let new_creds = crate::net::inspector::extract_credentials(&pkt);
if !new_creds.is_empty() {
self.credentials.extend(new_creds);
if self.credentials.len() > 1000 { self.credentials.drain(0..100); }
}
if self.recording {
if let Some(ref mut writer) = self.pcap_writer { let _ = writer.write_packet(&pkt); }
}
if self.filter.matches(&pkt) {
self.filtered.push(self.packets.len());
if self.selected.is_none() { self.selected = Some(0); }
}
self.packets.push_back(pkt);
if self.packets.len() > MAX_PACKETS {
self.packets.pop_front();
self.rebuild_filtered();
}
}
pub fn craft_inject(&mut self) {
let next_no = self.packet_counter + 1;
match self.craft.build_packet(next_no) {
Ok(pkt) => {
let label = format!("Injected #{} — {} {} → {}",
next_no, pkt.protocol, pkt.src, pkt.dst);
self.craft.result = Some(Ok(label));
self.inject_packet(pkt);
}
Err(e) => {
self.craft.result = Some(Err(e));
}
}
}
pub fn tick(&mut self) {
self.rate_tick += 1;
let flood_n = self.craft.flood_tick();
for i in 0..flood_n {
let next_no = self.packet_counter + 1 + i as u64;
if let Ok(pkt) = self.craft.build_packet(next_no) {
self.craft.flood_sent += 1;
self.inject_packet(pkt);
}
}
self.traceroute.tick();
if self.scan.running { self.scan.tick(); }
let replayed = self.replay.tick();
for pkt in replayed { self.ingest_packet_inner(pkt); }
if self.capturing {
if rand::random::<u8>() % 3 == 0 {
let entry = crate::sim::dynamic::generate_entry(self.rate_tick);
self.dyn_log.push(entry);
if self.dyn_log.len() > 500 { self.dyn_log.remove(0); }
}
}
if self.rate_tick % 10 == 0 {
self.rate_history.push(self.rate_this_sec);
self.rate_history.remove(0);
self.rate_this_sec = 0;
}
}
pub fn toggle_capture(&mut self) { self.capturing = !self.capturing; }
pub fn clear_packets(&mut self) {
self.capturing = false;
self.packets.clear();
self.filtered.clear();
self.selected = None;
self.total_bytes = 0;
self.packet_counter = 0;
self.flow_tracker.clear();
self.flows_selected = None;
self.stream_overlay = None;
self.security.clear();
self.credentials.clear();
}
pub fn toggle_recording(&mut self) {
if self.recording {
if let Some(ref mut w) = self.pcap_writer { let _ = w.flush(); }
self.pcap_writer = None;
self.recording = false;
} else {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default().as_secs();
let filename = format!("packrat_{}.pcap", ts);
let candidates = [
filename.clone(),
dirs_next::home_dir()
.map(|h| h.join(&filename).to_string_lossy().into_owned())
.unwrap_or_else(|| format!("/tmp/{}", filename)),
];
for path in &candidates {
if let Ok(writer) = PcapWriter::new(std::path::Path::new(path)) {
self.pcap_path = path.clone();
self.pcap_writer = Some(writer);
self.recording = true;
break;
}
}
}
}
pub fn rebuild_filtered(&mut self) {
self.filtered = self.packets.iter().enumerate()
.filter(|(_, p)| self.filter.matches(p))
.map(|(i, _)| i)
.collect();
if let Some(sel) = self.selected {
if sel >= self.filtered.len() {
self.selected = if self.filtered.is_empty() { None } else { Some(self.filtered.len() - 1) };
}
}
}
pub fn selected_packet(&self) -> Option<&Packet> {
self.selected.and_then(|i| self.filtered.get(i)).and_then(|&pi| self.packets.get(pi))
}
pub fn current_rate(&self) -> u32 { *self.rate_history.last().unwrap_or(&0) }
pub fn move_down(&mut self) {
match self.active_tab {
Tab::Packets => {
if let Some(sel) = self.selected {
if sel + 1 < self.filtered.len() { self.selected = Some(sel + 1); }
} else if !self.filtered.is_empty() { self.selected = Some(0); }
}
Tab::Analysis => { if self.analysis_section < 10 { self.analysis_section += 1; } }
Tab::Dynamic => { self.dyn_scroll = self.dyn_scroll.saturating_sub(1); }
Tab::Flows => {
let len = self.flow_tracker.flows.len();
if let Some(sel) = self.flows_selected {
if sel + 1 < len { self.flows_selected = Some(sel + 1); }
} else if len > 0 {
self.flows_selected = Some(0);
}
}
_ => {}
}
}
pub fn move_up(&mut self) {
match self.active_tab {
Tab::Packets => { if let Some(sel) = self.selected { if sel > 0 { self.selected = Some(sel - 1); } } }
Tab::Analysis => { if self.analysis_section > 0 { self.analysis_section -= 1; } }
Tab::Dynamic => {
let max = self.dyn_log.len().saturating_sub(1);
if self.dyn_scroll < max { self.dyn_scroll += 1; }
}
Tab::Flows => {
if let Some(sel) = self.flows_selected {
if sel > 0 { self.flows_selected = Some(sel - 1); }
}
}
_ => {}
}
}
pub fn flows_sort_bytes(&mut self) { self.flows_sort = FlowSort::Bytes; }
pub fn flows_sort_packets(&mut self) { self.flows_sort = FlowSort::Packets; }
pub fn flows_sort_time(&mut self) { self.flows_sort = FlowSort::Time; }
pub fn flows_sort_beacon(&mut self) { self.flows_sort = FlowSort::BeaconScore; }
pub fn flows_open_stream(&mut self) {
let sorted = self.flow_tracker.sorted_flows(&self.flows_sort);
if let Some(sel) = self.flows_selected {
if let Some(flow) = sorted.get(sel) {
let key = flow.key.clone();
let initiator = flow.initiator.clone();
let mut segments: Vec<(bool, Vec<u8>)> = Vec::new();
for pkt in &self.packets {
let pkt_key = FlowKey::from_packet(pkt);
if pkt_key == key {
let is_init = pkt.src == initiator;
let payload = if pkt.bytes.len() > 54 { pkt.bytes[54..].to_vec() } else { Vec::new() };
if !payload.is_empty() {
segments.push((is_init, payload));
}
}
}
let title = format!("{}:{} <-> {}:{} ({})",
key.ep1.0, key.ep1.1, key.ep2.0, key.ep2.1, key.proto);
self.stream_overlay = Some((title, segments));
}
}
}
pub fn flows_jump_to_packets(&mut self) {
let sorted = self.flow_tracker.sorted_flows(&self.flows_sort);
if let Some(sel) = self.flows_selected {
if let Some(flow) = sorted.get(sel) {
let ip = flow.key.ep1.0.clone();
self.filter.input = ip;
self.filter.active = false;
self.rebuild_filtered();
self.active_tab = crate::tabs::Tab::Packets;
}
}
}
pub fn move_top(&mut self) {
if matches!(self.active_tab, Tab::Packets) {
self.selected = if self.filtered.is_empty() { None } else { Some(0) };
}
}
pub fn move_bottom(&mut self) {
match self.active_tab {
Tab::Packets if !self.filtered.is_empty() => {
self.selected = Some(self.filtered.len() - 1);
}
Tab::Dynamic => {
self.dyn_scroll = 0; }
_ => {}
}
}
pub fn page_down(&mut self) {
if matches!(self.active_tab, Tab::Packets) {
if let Some(sel) = self.selected {
self.selected = Some((sel + 10).min(self.filtered.len().saturating_sub(1)));
}
}
}
pub fn page_up(&mut self) {
if matches!(self.active_tab, Tab::Packets) {
if let Some(sel) = self.selected { self.selected = Some(sel.saturating_sub(10)); }
}
}
pub fn next_tab(&mut self) {
let next = (self.active_tab.index() + 1) % Tab::COUNT;
self.active_tab = Tab::from_index(next);
}
pub fn security_subtab_next(&mut self) {
self.security_tab = match self.security_tab {
SecuritySubTab::Ids => SecuritySubTab::Credentials,
SecuritySubTab::Credentials => SecuritySubTab::OsFingerprint,
SecuritySubTab::OsFingerprint => SecuritySubTab::ArpWatch,
SecuritySubTab::ArpWatch => SecuritySubTab::DnsTunnel,
SecuritySubTab::DnsTunnel => SecuritySubTab::HttpAnalytics,
SecuritySubTab::HttpAnalytics => SecuritySubTab::TlsWeakness,
SecuritySubTab::TlsWeakness => SecuritySubTab::BruteForce,
SecuritySubTab::BruteForce => SecuritySubTab::VulnHits,
SecuritySubTab::VulnHits => SecuritySubTab::Replay,
SecuritySubTab::Replay => SecuritySubTab::Ids,
};
}
pub fn security_subtab_prev(&mut self) {
self.security_tab = match self.security_tab {
SecuritySubTab::Ids => SecuritySubTab::Replay,
SecuritySubTab::Credentials => SecuritySubTab::Ids,
SecuritySubTab::OsFingerprint => SecuritySubTab::Credentials,
SecuritySubTab::ArpWatch => SecuritySubTab::OsFingerprint,
SecuritySubTab::DnsTunnel => SecuritySubTab::ArpWatch,
SecuritySubTab::HttpAnalytics => SecuritySubTab::DnsTunnel,
SecuritySubTab::TlsWeakness => SecuritySubTab::HttpAnalytics,
SecuritySubTab::BruteForce => SecuritySubTab::TlsWeakness,
SecuritySubTab::VulnHits => SecuritySubTab::BruteForce,
SecuritySubTab::Replay => SecuritySubTab::VulnHits,
};
}
}