diff --git a/Justfile b/Justfile index e3d6b7e..0c8de83 100644 --- a/Justfile +++ b/Justfile @@ -14,10 +14,18 @@ show interface: sudo tc filter show dev $interface ingress sudo tc filter show dev $interface egress -# Run oryx +# Run oryx debug run: + cargo xtask run + +# Run oryx debug +release: cargo xtask run --release # Build oryx build: cargo xtask build + +# Profile +profile: + CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --root --bin oryx diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index c9d57b7..ae1cc1c 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -1,12 +1,6 @@ use oryx_common::RawPacket; use ratatui::{ - layout::{Alignment, Constraint, Direction, Flex, Layout, Margin, Rect}, - style::{Color, Style, Stylize}, - text::{Line, Span}, - widgets::{ - Block, BorderType, Borders, Cell, Clear, HighlightSpacing, Padding, Paragraph, Row, - Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, - }, + layout::{Constraint, Direction, Layout}, Frame, }; use std::{ @@ -14,44 +8,20 @@ use std::{ sync::{Arc, Mutex}, thread, }; -use tui_big_text::{BigText, PixelSize}; -use crate::alerts::alert::Alert; -use crate::bandwidth::Bandwidth; -use crate::filters::{ - filter::Filter, - fuzzy::{self, Fuzzy}, -}; -use crate::help::Help; -use crate::interface::Interface; use crate::notification::Notification; -use crate::packets::{ - network::{IpPacket, IpProto}, - packet::AppPacket, -}; -use crate::stats::Stats; +use crate::{filter::Filter, help::Help}; +use crate::{packet::AppPacket, section::Section}; pub type AppResult = std::result::Result>; pub const TICK_RATE: u64 = 40; #[derive(Debug, Copy, Clone, PartialEq)] -pub enum FocusedBlock { - Interface, - TransportFilter, - NetworkFilter, - LinkFilter, - TrafficDirection, - Start, +pub enum ActivePopup { Help, - Main, -} - -#[derive(Debug, PartialEq)] -pub enum Mode { - Packet, - Stats, - Alerts, + UpdateFilters, + PacketInfos, } #[derive(Debug)] @@ -64,27 +34,14 @@ pub struct DataEventHandler { pub struct App { pub running: bool, pub help: Help, - pub focused_block: FocusedBlock, - // used in setup to know which block to fall into after discarding help - pub previous_focused_block: FocusedBlock, - pub interface: Interface, pub filter: Filter, pub start_sniffing: bool, pub packets: Arc>>, - pub packets_table_state: TableState, - pub fuzzy: Arc>, pub notifications: Vec, - pub manuall_scroll: bool, - pub mode: Mode, - pub stats: Arc>, - pub packet_end_index: usize, - pub packet_window_size: usize, - pub update_filters: bool, + pub section: Section, pub data_channel_sender: kanal::Sender<[u8; RawPacket::LEN]>, - pub bandwidth: Bandwidth, - pub show_packet_infos_popup: bool, - pub packet_index: Option, - pub alert: Alert, + pub is_editing: bool, + pub active_popup: Option, } impl Default for App { @@ -96,17 +53,18 @@ impl Default for App { impl App { pub fn new() -> Self { let packets = Arc::new(Mutex::new(Vec::with_capacity(AppPacket::LEN * 1024 * 1024))); - let stats = Arc::new(Mutex::new(Stats::default())); let (sender, receiver) = kanal::unbounded(); - thread::spawn({ let packets = packets.clone(); - let stats = stats.clone(); - move || loop { if let Ok(raw_packet) = receiver.recv() { - App::process(packets.clone(), stats.clone(), AppPacket::from(raw_packet)); + let app_packet = AppPacket::from(raw_packet); + let mut packets = packets.lock().unwrap(); + if packets.len() == packets.capacity() { + packets.reserve(1024 * 1024); + } + packets.push(app_packet); } } }); @@ -114,626 +72,46 @@ impl App { Self { running: true, help: Help::new(), - focused_block: FocusedBlock::Interface, - previous_focused_block: FocusedBlock::Interface, - interface: Interface::default(), filter: Filter::new(), start_sniffing: false, packets: packets.clone(), - packets_table_state: TableState::default(), - fuzzy: Fuzzy::new(packets.clone()), notifications: Vec::new(), - manuall_scroll: false, - mode: Mode::Packet, - stats, - packet_end_index: 0, - packet_window_size: 0, - update_filters: false, + section: Section::new(packets.clone()), data_channel_sender: sender, - bandwidth: Bandwidth::new(), - show_packet_infos_popup: false, - packet_index: None, - alert: Alert::new(packets.clone()), + is_editing: false, + active_popup: None, } } pub fn render(&mut self, frame: &mut Frame) { // Setup if !self.start_sniffing { - let (interface_block, filter_block, start_block) = { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(self.interface.interfaces.len() as u16 + 6), - Constraint::Fill(1), - Constraint::Length(4), - ]) - .margin(1) - .flex(Flex::SpaceAround) - .split(frame.area()); - (chunks[0], chunks[1], chunks[2]) - }; - - // interfaces - self.interface - .render_on_setup(frame, interface_block, &self.focused_block); - - // Filters - self.filter - .render_on_setup(frame, filter_block, &self.focused_block); - - // Start Button - let start = BigText::builder() - .pixel_size(PixelSize::Sextant) - .style(if self.focused_block == FocusedBlock::Start { - Style::default().white().bold() - } else { - Style::default().dark_gray() - }) - .lines(vec!["START".into()]) - .centered() - .build(); - frame.render_widget(start, start_block); + self.filter.render_on_setup(frame); } else { // Sniffing - let (settings_block, mode_block) = { + let (settings_block, section_block) = { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(8), Constraint::Fill(1)]) .split(frame.area()); (chunks[0], chunks[1]) }; - // Settings - let (filter_block, interface_block) = { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .margin(1) - .split(settings_block); - (chunks[0], chunks[1]) - }; - - // Interface - self.interface.render_on_sniffing(frame, interface_block); - - // Filters - self.filter.render_on_sniffing(frame, filter_block); - - // Packets/Stats - match self.mode { - Mode::Packet => { - self.render_packets_mode(frame, mode_block); - if self.show_packet_infos_popup { - self.render_packet_infos_popup(frame); - } - } - Mode::Stats => self.render_stats_mode(frame, mode_block), - Mode::Alerts => self.alert.render(frame, mode_block), - } - - // Update filters - - if self.update_filters { - self.filter.update(frame, mode_block, &self.focused_block); - } - } - } - - pub fn render_packets_mode(&mut self, frame: &mut Frame, packet_mode_block: Rect) { - let app_packets = self.packets.lock().unwrap(); - let mut fuzzy = self.fuzzy.lock().unwrap(); - let fuzzy_packets = fuzzy.clone().packets.clone(); - - //TODO: ugly - let pattern = fuzzy.clone(); - let pattern = pattern.filter.value(); - - let (packet_block, fuzzy_block) = { - if fuzzy.is_enabled() { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(3)]) - .horizontal_margin(1) - .split(packet_mode_block); - (chunks[0], chunks[1]) - } else { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) - .horizontal_margin(1) - .split(packet_mode_block); - (chunks[0], chunks[1]) - } - }; - - let widths = [ - Constraint::Min(19), // Source Address - Constraint::Length(11), // Source Port - Constraint::Min(19), // Destination Address - Constraint::Length(16), // Destination Port - Constraint::Length(8), // Protocol - Constraint::Length(2), // Protocol - ]; - - // The size of the window where to display packets - let window_size = packet_mode_block.height.saturating_sub(5) as usize; - self.packet_window_size = window_size; - - // This points always to the end of the window - if self.packet_end_index < window_size { - self.packet_end_index = window_size; - } - - if fuzzy.packet_end_index < window_size { - fuzzy.packet_end_index = window_size; - } - - let packets_to_display = match self.manuall_scroll { - true => { - if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { - if fuzzy_packets.len() > window_size { - if let Some(selected_index) = fuzzy.scroll_state.selected() { - self.packet_index = Some( - fuzzy.packet_end_index.saturating_sub(window_size) + selected_index, - ); - } - &fuzzy_packets[fuzzy.packet_end_index.saturating_sub(window_size) - ..fuzzy.packet_end_index] - } else { - if let Some(selected_index) = fuzzy.scroll_state.selected() { - self.packet_index = Some(selected_index); - } else { - self.packet_index = None; - } - &fuzzy_packets - } - } else if app_packets.len() > window_size { - if let Some(selected_index) = self.packets_table_state.selected() { - self.packet_index = Some( - self.packet_end_index.saturating_sub(window_size) + selected_index, - ); - } - &app_packets - [self.packet_end_index.saturating_sub(window_size)..self.packet_end_index] - } else { - if let Some(selected_index) = self.packets_table_state.selected() { - self.packet_index = Some(selected_index); - } - &app_packets - } - } - false => { - if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { - if fuzzy_packets.len() > window_size { - self.packet_index = Some(fuzzy_packets.len().saturating_sub(1)); - &fuzzy_packets[fuzzy_packets.len().saturating_sub(window_size)..] - } else { - self.packet_index = Some(fuzzy_packets.len().saturating_sub(1)); - &fuzzy_packets - } - } else if app_packets.len() > window_size { - self.packet_index = Some(app_packets.len().saturating_sub(1)); - &app_packets[app_packets.len().saturating_sub(window_size)..] - } else { - self.packet_index = Some(app_packets.len().saturating_sub(1)); - &app_packets - } - } - }; - - // Style the packets - let packets: Vec = if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { - packets_to_display - .iter() - .map(|app_packet| match app_packet { - AppPacket::Arp(packet) => Row::new(vec![ - fuzzy::highlight(pattern, packet.src_mac.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, packet.dst_mac.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, "ARP".to_string()).cyan(), - ]), - AppPacket::Ip(packet) => match packet { - IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { - IpProto::Tcp(p) => Row::new(vec![ - fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), - fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), - fuzzy::highlight(pattern, "TCP".to_string()).cyan(), - ]), - IpProto::Udp(p) => Row::new(vec![ - fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), - fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), - fuzzy::highlight(pattern, "UDP".to_string()).cyan(), - ]), - IpProto::Icmp(_) => Row::new(vec![ - fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, "ICMP".to_string()).cyan(), - ]), - }, - IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { - IpProto::Tcp(p) => Row::new(vec![ - fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), - fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), - fuzzy::highlight(pattern, "TCP".to_string()).cyan(), - ]), - IpProto::Udp(p) => Row::new(vec![ - fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), - fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), - fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), - fuzzy::highlight(pattern, "UDP".to_string()).cyan(), - ]), - IpProto::Icmp(_) => Row::new(vec![ - fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), - Cell::from(Line::from("-").centered()).yellow(), - fuzzy::highlight(pattern, "ICMP".to_string()).cyan(), - ]), - }, - }, - }) - .collect() - } else { - packets_to_display - .iter() - .map(|app_packet| match app_packet { - AppPacket::Arp(packet) => Row::new(vec![ - Span::from(packet.src_mac.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from(packet.dst_mac.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from("ARP".to_string()).into_centered_line().cyan(), - ]), - AppPacket::Ip(packet) => match packet { - IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { - IpProto::Tcp(p) => Row::new(vec![ - Span::from(ipv4_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.src_port.to_string()) - .into_centered_line() - .yellow(), - Span::from(ipv4_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.dst_port.to_string()) - .into_centered_line() - .yellow(), - Span::from("TCP".to_string()).into_centered_line().cyan(), - ]), - IpProto::Udp(p) => Row::new(vec![ - Span::from(ipv4_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.src_port.to_string()) - .into_centered_line() - .yellow(), - Span::from(ipv4_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.dst_port.to_string()) - .into_centered_line() - .yellow(), - Span::from("UDP".to_string()).into_centered_line().cyan(), - ]), - IpProto::Icmp(_) => Row::new(vec![ - Span::from(ipv4_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from(ipv4_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from("ICMP".to_string()).into_centered_line().cyan(), - ]), - }, - IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { - IpProto::Tcp(p) => Row::new(vec![ - Span::from(ipv6_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.src_port.to_string()) - .into_centered_line() - .yellow(), - Span::from(ipv6_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.dst_port.to_string()) - .into_centered_line() - .yellow(), - Span::from("TCP".to_string()).into_centered_line().cyan(), - ]), - IpProto::Udp(p) => Row::new(vec![ - Span::from(ipv6_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.src_port.to_string()) - .into_centered_line() - .yellow(), - Span::from(ipv6_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from(p.dst_port.to_string()) - .into_centered_line() - .yellow(), - Span::from("UDP".to_string()).into_centered_line().cyan(), - ]), - IpProto::Icmp(_) => Row::new(vec![ - Span::from(ipv6_packet.src_ip.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from(ipv6_packet.dst_ip.to_string()) - .into_centered_line() - .blue(), - Span::from("-").into_centered_line().yellow(), - Span::from("ICMP".to_string()).into_centered_line().cyan(), - ]), - }, - }, - }) - .collect() - }; - // Always select the last packet - if !self.manuall_scroll { - if fuzzy.is_enabled() { - fuzzy.scroll_state.select(Some(packets_to_display.len())); - } else { - self.packets_table_state - .select(Some(packets_to_display.len())); - } - } - - let table = Table::new(packets, widths) - .header( - Row::new(vec![ - Line::from("Source Address").centered(), - Line::from("Source Port").centered(), - Line::from("Destination Address").centered(), - Line::from("Destination Port").centered(), - Line::from("Protocol").centered(), - { - if self.manuall_scroll { - Line::from(" ").centered().yellow() - } else { - Line::from("").centered() - } - }, - ]) - .style(Style::new().bold()) - .bottom_margin(1), - ) - .column_spacing(2) - .flex(Flex::SpaceBetween) - .highlight_style(Style::new().bg(ratatui::style::Color::DarkGray)) - .highlight_spacing(HighlightSpacing::Always) - .block( - Block::default() - .title({ - Line::from(vec![ - Span::styled( - " Packet ", - Style::default().bg(Color::Green).fg(Color::White).bold(), - ), - Span::from(" Stats ").fg(Color::DarkGray), - self.alert.title_span(), - ]) - }) - .title_alignment(Alignment::Left) - .padding(Padding::top(1)) - .borders(Borders::ALL) - .style(Style::default()) - .border_type(BorderType::default()) - .border_style(Style::default().green()), + self.section.render( + frame, + section_block, + &self.filter.interface.selected_interface.name, ); - if fuzzy.is_enabled() { - frame.render_stateful_widget(table, packet_block, &mut fuzzy.scroll_state); - } else { - frame.render_stateful_widget(table, packet_block, &mut self.packets_table_state); + self.filter.render_on_sniffing(frame, settings_block); } - - // Scrollbar - - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - - let mut scrollbar_state = if fuzzy.is_enabled() && fuzzy_packets.len() > window_size { - ScrollbarState::new(fuzzy_packets.len()).position({ - if self.manuall_scroll { - if fuzzy.packet_end_index == window_size { - 0 - } else { - fuzzy.packet_end_index - } - } else { - fuzzy.packets.len() - } - }) - } else if !fuzzy.is_enabled() && app_packets.len() > window_size { - ScrollbarState::new(app_packets.len()).position({ - if self.manuall_scroll { - if self.packet_end_index == window_size { - 0 - } else { - self.packet_end_index - } - } else { - app_packets.len() - } - }) - } else { - ScrollbarState::default() - }; - - frame.render_stateful_widget( - scrollbar, - packet_block.inner(Margin { - vertical: 1, - horizontal: 0, - }), - &mut scrollbar_state, - ); - - if fuzzy.is_enabled() { - let fuzzy = Paragraph::new(format!("> {}", fuzzy.filter.value())) - .alignment(Alignment::Left) - .style(Style::default().white()) - .block( - Block::new() - .borders(Borders::all()) - .title(" Search  ") - .title_style({ - if fuzzy.is_paused() { - Style::default().bold().green() - } else { - Style::default().bold().yellow() - } - }) - .border_style({ - if fuzzy.is_paused() { - Style::default().green() - } else { - Style::default().yellow() - } - }), - ); - - frame.render_widget(fuzzy, fuzzy_block); - } - } - - pub fn render_stats_mode(&mut self, frame: &mut Frame, block: Rect) { - let stats = self.stats.lock().unwrap(); - - let (bandwidth_block, stats_block) = { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .margin(2) - .split(block); - (chunks[0], chunks[1]) - }; - - frame.render_widget( - Block::default() - .title({ - Line::from(vec![ - Span::from(" Packet ").fg(Color::DarkGray), - Span::styled( - " Stats ", - Style::default().bg(Color::Green).fg(Color::White).bold(), - ), - self.alert.title_span(), - ]) - }) - .title_alignment(Alignment::Left) - .padding(Padding::top(1)) - .borders(Borders::ALL) - .style(Style::default()) - .border_type(BorderType::default()) - .border_style(Style::default().green()), - block.inner(Margin { - horizontal: 1, - vertical: 0, - }), - ); - - stats.render(frame, stats_block); - - self.bandwidth.render( - frame, - bandwidth_block, - &self.interface.selected_interface.name.clone(), - ); - } - - fn render_packet_infos_popup(&self, frame: &mut Frame) { - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Length(36), - Constraint::Fill(1), - ]) - .flex(ratatui::layout::Flex::SpaceBetween) - .split(frame.area()); - - let block = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Fill(1), - Constraint::Max(80), - Constraint::Fill(1), - ]) - .flex(ratatui::layout::Flex::SpaceBetween) - .split(layout[1])[1]; - - let fuzzy = self.fuzzy.lock().unwrap(); - let packets = self.packets.lock().unwrap(); - - let packet = if fuzzy.is_enabled() { - fuzzy.packets[self.packet_index.unwrap()] - } else { - packets[self.packet_index.unwrap()] - }; - - frame.render_widget(Clear, block); - frame.render_widget( - Block::new() - .title(" Packet Infos 󰋼 ") - .title_style(Style::new().bold().green()) - .title_alignment(Alignment::Center) - .borders(Borders::all()) - .border_style(Style::new().green()) - .border_type(BorderType::Thick), - block, - ); - match packet { - AppPacket::Ip(ip_packet) => ip_packet.render(block, frame), - AppPacket::Arp(arp_packet) => arp_packet.render(block, frame), - }; - } - - pub fn process( - packets: Arc>>, - stats: Arc>, - app_packet: AppPacket, - ) { - let mut packets = packets.lock().unwrap(); - - if packets.len() == packets.capacity() { - packets.reserve(1024 * 1024); - } - - packets.push(app_packet); - - let mut stats = stats.lock().unwrap(); - stats.refresh(&app_packet); } pub fn tick(&mut self) { self.notifications.iter_mut().for_each(|n| n.ttl -= 1); self.notifications.retain(|n| n.ttl > 0); - self.alert.check(); + self.section.alert.check(); } pub fn quit(&mut self) { diff --git a/oryx-tui/src/export.rs b/oryx-tui/src/export.rs index e93ac93..78599ca 100644 --- a/oryx-tui/src/export.rs +++ b/oryx-tui/src/export.rs @@ -6,9 +6,9 @@ use std::{ use crate::{ app::AppResult, - packets::{ + packet::{ network::{IpPacket, IpProto}, - packet::AppPacket, + AppPacket, }, }; diff --git a/oryx-tui/src/filter.rs b/oryx-tui/src/filter.rs new file mode 100644 index 0000000..d6c9ea6 --- /dev/null +++ b/oryx-tui/src/filter.rs @@ -0,0 +1,731 @@ +pub mod direction; +pub mod fuzzy; +mod link; +mod network; +mod transport; + +use std::{thread, time::Duration}; + +use crossterm::event::{KeyCode, KeyEvent}; +use direction::{TrafficDirection, TrafficDirectionFilter}; +use link::LinkFilter; +use network::NetworkFilter; +use oryx_common::{ + protocols::{ + LinkProtocol, NetworkProtocol, Protocol, TransportProtocol, NB_LINK_PROTOCOL, + NB_NETWORK_PROTOCOL, NB_TRANSPORT_PROTOCOL, + }, + RawPacket, +}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Clear, Padding, Row, Table, TableState}, + Frame, +}; +use transport::TransportFilter; +use tui_big_text::{BigText, PixelSize}; + +use crate::{app::AppResult, ebpf::Ebpf, event::Event, interface::Interface}; + +#[derive(Debug, Clone)] +pub struct FilterChannel { + pub sender: kanal::Sender<(Protocol, bool)>, + pub receiver: kanal::Receiver<(Protocol, bool)>, +} + +impl FilterChannel { + pub fn new() -> Self { + let (sender, receiver) = kanal::unbounded(); + Self { sender, receiver } + } +} + +impl Default for FilterChannel { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum FocusedBlock { + Interface, + TransportFilter, + NetworkFilter, + LinkFilter, + TrafficDirection, + Apply, +} + +#[derive(Debug)] +pub struct Filter { + pub interface: Interface, + pub network: NetworkFilter, + pub transport: TransportFilter, + pub link: LinkFilter, + pub traffic_direction: TrafficDirectionFilter, + pub ingress_channel: FilterChannel, + pub egress_channel: FilterChannel, + pub focused_block: FocusedBlock, +} + +impl Default for Filter { + fn default() -> Self { + Self::new() + } +} + +impl Filter { + pub fn new() -> Self { + Self { + interface: Interface::new(), + network: NetworkFilter::new(), + transport: TransportFilter::new(), + link: LinkFilter::new(), + traffic_direction: TrafficDirectionFilter::new(), + ingress_channel: FilterChannel::new(), + egress_channel: FilterChannel::new(), + focused_block: FocusedBlock::Interface, + } + } + + pub fn terminate(&mut self) { + self.traffic_direction.terminate(TrafficDirection::Egress); + self.traffic_direction.terminate(TrafficDirection::Ingress); + } + + pub fn start( + &mut self, + notification_sender: kanal::Sender, + data_sender: kanal::Sender<[u8; RawPacket::LEN]>, + ) { + let iface = self.interface.selected_interface.name.clone(); + + self.apply(); + + if self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Ingress) + { + Ebpf::load_ingress( + iface.clone(), + notification_sender.clone(), + data_sender.clone(), + self.ingress_channel.receiver.clone(), + self.traffic_direction.terminate_ingress.clone(), + ); + } + + if self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Egress) + { + Ebpf::load_egress( + iface, + notification_sender, + data_sender, + self.egress_channel.receiver.clone(), + self.traffic_direction.terminate_egress.clone(), + ); + } + } + + pub fn trigger(&mut self) { + self.network.selected_protocols = self.network.applied_protocols.clone(); + + self.transport.selected_protocols = self.transport.applied_protocols.clone(); + + self.link.selected_protocols = self.link.applied_protocols.clone(); + + self.traffic_direction.selected_direction = + self.traffic_direction.applied_direction.clone(); + + self.transport.state = TableState::default().with_selected(0); + + self.focused_block = FocusedBlock::TransportFilter; + } + + pub fn sync(&mut self) -> AppResult<()> { + for protocol in TransportProtocol::all().iter() { + if self.transport.applied_protocols.contains(protocol) { + self.ingress_channel + .sender + .send((Protocol::Transport(*protocol), false))?; + self.egress_channel + .sender + .send((Protocol::Transport(*protocol), false))?; + } else { + self.ingress_channel + .sender + .send((Protocol::Transport(*protocol), true))?; + self.egress_channel + .sender + .send((Protocol::Transport(*protocol), true))?; + } + } + + for protocol in NetworkProtocol::all().iter() { + if self.network.applied_protocols.contains(protocol) { + self.ingress_channel + .sender + .send((Protocol::Network(*protocol), false))?; + self.egress_channel + .sender + .send((Protocol::Network(*protocol), false))?; + } else { + self.ingress_channel + .sender + .send((Protocol::Network(*protocol), true))?; + self.egress_channel + .sender + .send((Protocol::Network(*protocol), true))?; + } + } + + for protocol in LinkProtocol::all().iter() { + if self.link.applied_protocols.contains(protocol) { + self.ingress_channel + .sender + .send((Protocol::Link(*protocol), false))?; + self.egress_channel + .sender + .send((Protocol::Link(*protocol), false))?; + } else { + self.ingress_channel + .sender + .send((Protocol::Link(*protocol), true))?; + self.egress_channel + .sender + .send((Protocol::Link(*protocol), true))?; + } + } + + Ok(()) + } + + pub fn update( + &mut self, + notification_sender: kanal::Sender, + data_sender: kanal::Sender<[u8; RawPacket::LEN]>, + ) -> AppResult<()> { + // Remove egress + if self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Egress) + && !self + .traffic_direction + .selected_direction + .contains(&TrafficDirection::Egress) + { + self.traffic_direction.terminate(TrafficDirection::Egress); + } + + // Add egress + if !self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Egress) + && self + .traffic_direction + .selected_direction + .contains(&TrafficDirection::Egress) + { + self.traffic_direction + .terminate_egress + .store(false, std::sync::atomic::Ordering::Relaxed); + + let iface = self.interface.selected_interface.name.clone(); + + Ebpf::load_egress( + iface, + notification_sender.clone(), + data_sender.clone(), + self.egress_channel.receiver.clone(), + self.traffic_direction.terminate_egress.clone(), + ); + } + + // Remove ingress + if self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Ingress) + && !self + .traffic_direction + .selected_direction + .contains(&TrafficDirection::Ingress) + { + self.traffic_direction.terminate(TrafficDirection::Ingress); + } + + // Add ingress + if !self + .traffic_direction + .applied_direction + .contains(&TrafficDirection::Ingress) + && self + .traffic_direction + .selected_direction + .contains(&TrafficDirection::Ingress) + { + let iface = self.interface.selected_interface.name.clone(); + self.traffic_direction + .terminate_ingress + .store(false, std::sync::atomic::Ordering::Relaxed); + Ebpf::load_ingress( + iface, + notification_sender.clone(), + data_sender.clone(), + self.ingress_channel.receiver.clone(), + self.traffic_direction.terminate_ingress.clone(), + ); + } + + self.apply(); + + thread::sleep(Duration::from_millis(150)); + + self.traffic_direction + .terminate_ingress + .store(false, std::sync::atomic::Ordering::Relaxed); + self.traffic_direction + .terminate_ingress + .store(false, std::sync::atomic::Ordering::Relaxed); + + self.sync()?; + + Ok(()) + } + + pub fn handle_key_events(&mut self, key_event: KeyEvent, is_update_popup_displayed: bool) { + match key_event.code { + KeyCode::Tab => match self.focused_block { + FocusedBlock::Interface => { + self.focused_block = FocusedBlock::TransportFilter; + self.interface.state.select(None); + self.transport.state.select(Some(0)); + } + FocusedBlock::TransportFilter => { + self.focused_block = FocusedBlock::NetworkFilter; + self.network.state.select(Some(0)); + self.transport.state.select(None); + } + + FocusedBlock::NetworkFilter => { + self.focused_block = FocusedBlock::LinkFilter; + self.link.state.select(Some(0)); + self.network.state.select(None); + } + + FocusedBlock::LinkFilter => { + self.focused_block = FocusedBlock::TrafficDirection; + self.traffic_direction.state.select(Some(0)); + self.link.state.select(None); + } + + FocusedBlock::TrafficDirection => { + self.focused_block = FocusedBlock::Apply; + self.traffic_direction.state.select(None); + } + + FocusedBlock::Apply => { + if is_update_popup_displayed { + self.focused_block = FocusedBlock::TransportFilter; + } else { + self.focused_block = FocusedBlock::Interface; + self.interface.state.select(Some(0)); + } + } + }, + KeyCode::BackTab => match &self.focused_block { + FocusedBlock::Interface => { + self.focused_block = FocusedBlock::Apply; + self.interface.state.select(None); + } + + FocusedBlock::TransportFilter => { + if is_update_popup_displayed { + self.focused_block = FocusedBlock::Apply; + self.transport.state.select(None); + } else { + self.focused_block = FocusedBlock::Interface; + self.interface.state.select(Some(0)); + self.transport.state.select(None); + } + } + + FocusedBlock::NetworkFilter => { + self.focused_block = FocusedBlock::TransportFilter; + self.transport.state.select(Some(0)); + self.network.state.select(None); + } + + FocusedBlock::LinkFilter => { + self.focused_block = FocusedBlock::NetworkFilter; + self.network.state.select(Some(0)); + self.link.state.select(None); + } + + FocusedBlock::TrafficDirection => { + self.focused_block = FocusedBlock::LinkFilter; + self.link.state.select(Some(0)); + self.traffic_direction.state.select(None); + } + + FocusedBlock::Apply => { + self.focused_block = FocusedBlock::TrafficDirection; + self.traffic_direction.state.select(Some(0)); + } + }, + + KeyCode::Char('j') | KeyCode::Down => match &self.focused_block { + FocusedBlock::Interface => { + self.interface.scroll_down(); + } + FocusedBlock::TransportFilter => { + self.transport.scroll_down(); + } + + FocusedBlock::NetworkFilter => { + self.network.scroll_down(); + } + + FocusedBlock::LinkFilter => { + self.link.scroll_down(); + } + + FocusedBlock::TrafficDirection => { + self.traffic_direction.state.select(Some(1)); + } + _ => {} + }, + + KeyCode::Char('k') | KeyCode::Up => match self.focused_block { + FocusedBlock::Interface => { + self.interface.scroll_up(); + } + FocusedBlock::TransportFilter => { + self.transport.scroll_up(); + } + + FocusedBlock::NetworkFilter => { + self.network.scroll_up(); + } + + FocusedBlock::LinkFilter => { + self.link.scroll_up(); + } + + FocusedBlock::TrafficDirection => { + self.traffic_direction.state.select(Some(0)); + } + _ => {} + }, + + KeyCode::Char(' ') => match &self.focused_block { + FocusedBlock::Interface => { + if let Some(index) = self.interface.state.selected() { + let net_interface = self.interface.interfaces[index].clone(); + if net_interface.is_up { + self.interface.selected_interface = + self.interface.interfaces[index].clone(); + } + } + } + FocusedBlock::NetworkFilter => { + self.network.select(); + } + + FocusedBlock::TransportFilter => { + self.transport.select(); + } + + FocusedBlock::LinkFilter => { + self.link.select(); + } + + FocusedBlock::TrafficDirection => { + self.traffic_direction.select(); + } + + _ => {} + }, + + _ => {} + } + } + + pub fn apply(&mut self) { + self.network.apply(); + self.transport.apply(); + self.link.apply(); + self.traffic_direction.apply(); + } + + pub fn render_on_setup(&mut self, frame: &mut Frame) { + let ( + interface_block, + transport_filter_block, + network_filter_block, + link_filter_block, + traffic_direction_block, + start_block, + ) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(self.interface.interfaces.len() as u16 + 6), + Constraint::Length(NB_TRANSPORT_PROTOCOL + 4), + Constraint::Length(NB_NETWORK_PROTOCOL + 4), + Constraint::Length(NB_LINK_PROTOCOL + 4), + Constraint::Length(6), + Constraint::Length(4), + ]) + .margin(1) + .flex(Flex::SpaceAround) + .split(frame.area()); + ( + chunks[0], chunks[1], chunks[2], chunks[3], chunks[4], chunks[5], + ) + }; + + self.interface.render_on_setup( + frame, + interface_block, + self.focused_block == FocusedBlock::Interface, + ); + + self.network.render( + frame, + network_filter_block, + self.focused_block == FocusedBlock::NetworkFilter, + ); + + self.transport.render( + frame, + transport_filter_block, + self.focused_block == FocusedBlock::TransportFilter, + ); + + self.link.render( + frame, + link_filter_block, + self.focused_block == FocusedBlock::LinkFilter, + ); + + self.traffic_direction.render( + frame, + traffic_direction_block, + self.focused_block == FocusedBlock::TrafficDirection, + ); + + let start = BigText::builder() + .pixel_size(PixelSize::Sextant) + .style(if self.focused_block == FocusedBlock::Apply { + Style::default().white().bold() + } else { + Style::default().dark_gray() + }) + .lines(vec!["START".into()]) + .centered() + .build(); + + frame.render_widget(start, start_block); + } + + pub fn render_on_sniffing(&mut self, frame: &mut Frame, block: Rect) { + let (filter_summury_block, interface_block) = { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .margin(1) + .split(block); + (chunks[0], chunks[1]) + }; + + self.interface.render_on_sniffing(frame, interface_block); + + let widths = [Constraint::Length(10), Constraint::Fill(1)]; + let filters = { + [ + Row::new(vec![ + Line::styled("Transport", Style::new().bold()), + Line::from_iter(TransportFilter::new().selected_protocols.iter().map( + |filter| { + if self.transport.applied_protocols.contains(filter) { + Span::styled( + format!(" {} ", filter), + Style::default().light_green(), + ) + } else { + Span::styled( + format!(" {} ", filter), + Style::default().light_red(), + ) + } + }, + )), + ]), + Row::new(vec![ + Line::styled("Network", Style::new().bold()), + Line::from_iter( + NetworkFilter::new() + .selected_protocols + .iter() + .map(|filter| { + if self.network.applied_protocols.contains(filter) { + Span::styled( + format!(" {} ", filter), + Style::default().light_green(), + ) + } else { + Span::styled( + format!(" {} ", filter), + Style::default().light_red(), + ) + } + }), + ), + ]), + Row::new(vec![ + Line::styled("Link", Style::new().bold()), + Line::from_iter(LinkFilter::new().selected_protocols.iter().map(|filter| { + if self.link.applied_protocols.contains(filter) { + Span::styled(format!(" {} ", filter), Style::default().light_green()) + } else { + Span::styled(format!(" {} ", filter), Style::default().light_red()) + } + })), + ]), + Row::new(vec![ + Line::styled("Direction", Style::new().bold()), + Line::from_iter( + TrafficDirectionFilter::default() + .selected_direction + .iter() + .map(|filter| { + if self.traffic_direction.applied_direction.contains(filter) { + Span::styled( + format!("󰞁 {} ", filter), + Style::default().light_green(), + ) + } else { + Span::styled( + format!("󰿝 {} ", filter), + Style::default().light_red(), + ) + } + }), + ), + ]), + ] + }; + + let table = Table::new(filters, widths).column_spacing(3).block( + Block::default() + .title(" Filters 󱪤 ") + .title_style(Style::default().bold().green()) + .title_alignment(Alignment::Center) + .padding(Padding::horizontal(2)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + ); + + frame.render_widget(table, filter_summury_block); + } + + pub fn render_update_popup(&mut self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(40), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(60), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let ( + transport_filter_block, + network_filter_block, + link_filter_block, + traffic_direction_block, + apply_block, + ) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(NB_TRANSPORT_PROTOCOL + 4), + Constraint::Length(NB_NETWORK_PROTOCOL + 4), + Constraint::Length(NB_LINK_PROTOCOL + 4), + Constraint::Length(6), + Constraint::Length(4), + ]) + .margin(1) + .flex(Flex::SpaceBetween) + .split(block); + (chunks[0], chunks[1], chunks[2], chunks[3], chunks[4]) + }; + + frame.render_widget(Clear, block); + frame.render_widget( + Block::new() + .borders(Borders::all()) + .border_type(BorderType::Thick) + .border_style(Style::default().green()), + block, + ); + + self.network.render( + frame, + network_filter_block, + self.focused_block == FocusedBlock::NetworkFilter, + ); + + self.transport.render( + frame, + transport_filter_block, + self.focused_block == FocusedBlock::TransportFilter, + ); + + self.link.render( + frame, + link_filter_block, + self.focused_block == FocusedBlock::LinkFilter, + ); + + self.traffic_direction.render( + frame, + traffic_direction_block, + self.focused_block == FocusedBlock::TrafficDirection, + ); + + let apply = BigText::builder() + .pixel_size(PixelSize::Sextant) + .style(if self.focused_block == FocusedBlock::Apply { + Style::default().white().bold() + } else { + Style::default().dark_gray() + }) + .lines(vec!["APPLY".into()]) + .centered() + .build(); + frame.render_widget(apply, apply_block); + } +} diff --git a/oryx-tui/src/filters/direction.rs b/oryx-tui/src/filter/direction.rs similarity index 95% rename from oryx-tui/src/filters/direction.rs rename to oryx-tui/src/filter/direction.rs index 9107a40..95104ce 100644 --- a/oryx-tui/src/filters/direction.rs +++ b/oryx-tui/src/filter/direction.rs @@ -13,8 +13,6 @@ use ratatui::{ Frame, }; -use crate::app::FocusedBlock; - #[derive(Debug)] pub struct TrafficDirectionFilter { pub state: TableState, @@ -84,7 +82,7 @@ impl TrafficDirectionFilter { self.selected_direction.clear(); } - pub fn render(&mut self, frame: &mut Frame, block: Rect, focused_block: &FocusedBlock) { + pub fn render(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints([ @@ -130,7 +128,7 @@ impl TrafficDirectionFilter { .title_style(Style::default().bold().fg(Color::Green)) .title_alignment(Alignment::Center) .borders(Borders::LEFT) - .border_type(if *focused_block == FocusedBlock::TrafficDirection { + .border_type(if is_focused { BorderType::Thick } else { BorderType::default() diff --git a/oryx-tui/src/filters/fuzzy.rs b/oryx-tui/src/filter/fuzzy.rs similarity index 73% rename from oryx-tui/src/filters/fuzzy.rs rename to oryx-tui/src/filter/fuzzy.rs index e26af39..124b45a 100644 --- a/oryx-tui/src/filters/fuzzy.rs +++ b/oryx-tui/src/filter/fuzzy.rs @@ -11,7 +11,7 @@ use ratatui::{ }; use tui_input::Input; -use crate::{app::TICK_RATE, packets::packet::AppPacket}; +use crate::{app::TICK_RATE, packet::AppPacket}; #[derive(Debug, Clone, Default)] pub struct Fuzzy { @@ -56,6 +56,44 @@ impl Fuzzy { fuzzy } + pub fn scroll_down(&mut self, win_size: usize) { + let i = match self.scroll_state.selected() { + Some(i) => { + if i < win_size - 1 { + i + 1 + } else if i == win_size - 1 && self.packets.len() > self.packet_end_index { + // shit the window by one + self.packet_end_index += 1; + i + 1 + } else { + i + } + } + None => self.packets.len(), + }; + + self.scroll_state.select(Some(i)); + } + + pub fn scroll_up(&mut self, win_size: usize) { + let i = match self.scroll_state.selected() { + Some(i) => { + if i > 1 { + i - 1 + } else if i == 0 && self.packet_end_index > win_size { + // shit the window by one + self.packet_end_index -= 1; + 0 + } else { + 0 + } + } + None => self.packets.len(), + }; + + self.scroll_state.select(Some(i)); + } + pub fn find(&mut self, packets: &[AppPacket]) { self.packets = packets .iter() diff --git a/oryx-tui/src/filters/link.rs b/oryx-tui/src/filter/link.rs similarity index 94% rename from oryx-tui/src/filters/link.rs rename to oryx-tui/src/filter/link.rs index 2623ac3..cfe09cd 100644 --- a/oryx-tui/src/filters/link.rs +++ b/oryx-tui/src/filter/link.rs @@ -6,8 +6,6 @@ use ratatui::{ Frame, }; -use crate::app::FocusedBlock; - #[derive(Debug)] pub struct LinkFilter { pub state: TableState, @@ -75,7 +73,7 @@ impl LinkFilter { self.selected_protocols.clear(); } - pub fn render(&mut self, frame: &mut Frame, block: Rect, focused_block: &FocusedBlock) { + pub fn render(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints( @@ -112,7 +110,7 @@ impl LinkFilter { .title_style(Style::default().bold().fg(Color::Green)) .title_alignment(Alignment::Center) .borders(Borders::LEFT) - .border_type(if *focused_block == FocusedBlock::LinkFilter { + .border_type(if is_focused { BorderType::Thick } else { BorderType::default() diff --git a/oryx-tui/src/filters/network.rs b/oryx-tui/src/filter/network.rs similarity index 95% rename from oryx-tui/src/filters/network.rs rename to oryx-tui/src/filter/network.rs index 1399953..0bac490 100644 --- a/oryx-tui/src/filters/network.rs +++ b/oryx-tui/src/filter/network.rs @@ -6,8 +6,6 @@ use ratatui::{ Frame, }; -use crate::app::FocusedBlock; - #[derive(Debug)] pub struct NetworkFilter { pub state: TableState, @@ -85,7 +83,7 @@ impl NetworkFilter { self.selected_protocols.clear(); } - pub fn render(&mut self, frame: &mut Frame, block: Rect, focused_block: &FocusedBlock) { + pub fn render(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints( @@ -144,7 +142,7 @@ impl NetworkFilter { .title_style(Style::default().bold().fg(Color::Green)) .title_alignment(Alignment::Center) .borders(Borders::LEFT) - .border_type(if *focused_block == FocusedBlock::NetworkFilter { + .border_type(if is_focused { BorderType::Thick } else { BorderType::default() diff --git a/oryx-tui/src/filters/transport.rs b/oryx-tui/src/filter/transport.rs similarity index 95% rename from oryx-tui/src/filters/transport.rs rename to oryx-tui/src/filter/transport.rs index 1a0da9c..47fac48 100644 --- a/oryx-tui/src/filters/transport.rs +++ b/oryx-tui/src/filter/transport.rs @@ -6,8 +6,6 @@ use ratatui::{ Frame, }; -use crate::app::FocusedBlock; - #[derive(Debug)] pub struct TransportFilter { pub state: TableState, @@ -80,7 +78,7 @@ impl TransportFilter { self.selected_protocols.clear(); } - pub fn render(&mut self, frame: &mut Frame, block: Rect, focused_block: &FocusedBlock) { + pub fn render(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints( @@ -129,7 +127,7 @@ impl TransportFilter { .title_style(Style::default().bold().fg(Color::Green)) .title_alignment(Alignment::Center) .borders(Borders::LEFT) - .border_type(if *focused_block == FocusedBlock::TransportFilter { + .border_type(if is_focused { BorderType::Thick } else { BorderType::default() diff --git a/oryx-tui/src/filters/filter.rs b/oryx-tui/src/filters/filter.rs deleted file mode 100644 index 0776156..0000000 --- a/oryx-tui/src/filters/filter.rs +++ /dev/null @@ -1,272 +0,0 @@ -use oryx_common::protocols::{ - Protocol, NB_LINK_PROTOCOL, NB_NETWORK_PROTOCOL, NB_TRANSPORT_PROTOCOL, -}; -use ratatui::{ - layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, - style::{Style, Stylize}, - text::{Line, Span}, - widgets::{Block, BorderType, Borders, Clear, Padding, Row, Table}, - Frame, -}; -use tui_big_text::{BigText, PixelSize}; - -use crate::app::FocusedBlock; - -use super::{ - direction::TrafficDirectionFilter, link::LinkFilter, network::NetworkFilter, - transport::TransportFilter, -}; - -#[derive(Debug, Clone)] -pub struct FilterChannel { - pub sender: kanal::Sender<(Protocol, bool)>, - pub receiver: kanal::Receiver<(Protocol, bool)>, -} - -impl FilterChannel { - pub fn new() -> Self { - let (sender, receiver) = kanal::unbounded(); - Self { sender, receiver } - } -} - -impl Default for FilterChannel { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug)] -pub struct Filter { - pub network: NetworkFilter, - pub transport: TransportFilter, - pub link: LinkFilter, - pub traffic_direction: TrafficDirectionFilter, - pub ingress_channel: FilterChannel, - pub egress_channel: FilterChannel, -} - -impl Default for Filter { - fn default() -> Self { - Self::new() - } -} - -impl Filter { - pub fn new() -> Self { - Self { - network: NetworkFilter::new(), - transport: TransportFilter::new(), - link: LinkFilter::new(), - traffic_direction: TrafficDirectionFilter::new(), - ingress_channel: FilterChannel::new(), - egress_channel: FilterChannel::new(), - } - } - - pub fn render_on_setup( - &mut self, - frame: &mut Frame, - block: Rect, - focused_block: &FocusedBlock, - ) { - let ( - transport_filter_block, - network_filter_block, - link_filter_block, - traffic_direction_block, - ) = { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(NB_TRANSPORT_PROTOCOL + 4), - Constraint::Length(NB_NETWORK_PROTOCOL + 4), - Constraint::Length(NB_LINK_PROTOCOL + 4), - Constraint::Length(6), - ]) - .margin(1) - .flex(Flex::SpaceAround) - .split(block); - (chunks[0], chunks[1], chunks[2], chunks[3]) - }; - - self.network - .render(frame, network_filter_block, focused_block); - - self.transport - .render(frame, transport_filter_block, focused_block); - - self.link.render(frame, link_filter_block, focused_block); - - self.traffic_direction - .render(frame, traffic_direction_block, focused_block); - } - - pub fn render_on_sniffing(&mut self, frame: &mut Frame, block: Rect) { - let widths = [Constraint::Length(10), Constraint::Fill(1)]; - let filters = { - [ - Row::new(vec![ - Line::styled("Transport", Style::new().bold()), - Line::from_iter(TransportFilter::new().selected_protocols.iter().map( - |filter| { - if self.transport.applied_protocols.contains(filter) { - Span::styled( - format!(" {} ", filter), - Style::default().light_green(), - ) - } else { - Span::styled( - format!(" {} ", filter), - Style::default().light_red(), - ) - } - }, - )), - ]), - Row::new(vec![ - Line::styled("Network", Style::new().bold()), - Line::from_iter( - NetworkFilter::new() - .selected_protocols - .iter() - .map(|filter| { - if self.network.applied_protocols.contains(filter) { - Span::styled( - format!(" {} ", filter), - Style::default().light_green(), - ) - } else { - Span::styled( - format!(" {} ", filter), - Style::default().light_red(), - ) - } - }), - ), - ]), - Row::new(vec![ - Line::styled("Link", Style::new().bold()), - Line::from_iter(LinkFilter::new().selected_protocols.iter().map(|filter| { - if self.link.applied_protocols.contains(filter) { - Span::styled(format!(" {} ", filter), Style::default().light_green()) - } else { - Span::styled(format!(" {} ", filter), Style::default().light_red()) - } - })), - ]), - Row::new(vec![ - Line::styled("Direction", Style::new().bold()), - Line::from_iter( - TrafficDirectionFilter::default() - .selected_direction - .iter() - .map(|filter| { - if self.traffic_direction.applied_direction.contains(filter) { - Span::styled( - format!("󰞁 {} ", filter), - Style::default().light_green(), - ) - } else { - Span::styled( - format!("󰿝 {} ", filter), - Style::default().light_red(), - ) - } - }), - ), - ]), - ] - }; - - let table = Table::new(filters, widths).column_spacing(3).block( - Block::default() - .title(" Filters 󱪤 ") - .title_style(Style::default().bold().green()) - .title_alignment(Alignment::Center) - .padding(Padding::horizontal(2)) - .borders(Borders::ALL) - .style(Style::default()) - .border_type(BorderType::default()) - .border_style(Style::default().green()), - ); - - frame.render_widget(table, block); - } - - pub fn update(&mut self, frame: &mut Frame, block: Rect, focused_block: &FocusedBlock) { - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Length(40), - Constraint::Fill(1), - ]) - .flex(ratatui::layout::Flex::SpaceBetween) - .split(block); - - let block = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Fill(1), - Constraint::Length(60), - Constraint::Fill(1), - ]) - .flex(ratatui::layout::Flex::SpaceBetween) - .split(layout[1])[1]; - - let ( - transport_filter_block, - network_filter_block, - link_filter_block, - traffic_direction_block, - apply_block, - ) = { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(NB_TRANSPORT_PROTOCOL + 4), - Constraint::Length(NB_NETWORK_PROTOCOL + 4), - Constraint::Length(NB_LINK_PROTOCOL + 4), - Constraint::Length(6), - Constraint::Length(4), - ]) - .margin(1) - .flex(Flex::SpaceBetween) - .split(block); - (chunks[0], chunks[1], chunks[2], chunks[3], chunks[4]) - }; - - frame.render_widget(Clear, block); - frame.render_widget( - Block::new() - .borders(Borders::all()) - .border_type(BorderType::Thick) - .border_style(Style::default().green()), - block, - ); - - self.transport - .render(frame, transport_filter_block, focused_block); - - self.network - .render(frame, network_filter_block, focused_block); - - self.link.render(frame, link_filter_block, focused_block); - - self.traffic_direction - .render(frame, traffic_direction_block, focused_block); - - let apply = BigText::builder() - .pixel_size(PixelSize::Sextant) - .style(if *focused_block == FocusedBlock::Start { - Style::default().white().bold() - } else { - Style::default().dark_gray() - }) - .lines(vec!["APPLY".into()]) - .centered() - .build(); - frame.render_widget(apply, apply_block); - } -} diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index 8efee88..6ffa71d 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -1,993 +1,162 @@ -use oryx_common::protocols::{LinkProtocol, NetworkProtocol, Protocol, TransportProtocol}; use std::{thread, time::Duration}; -use tui_input::backend::crossterm::EventHandler; use crate::{ - app::{App, AppResult, FocusedBlock, Mode}, - ebpf::Ebpf, + app::{ActivePopup, App, AppResult}, event::Event, export::export, - filters::direction::TrafficDirection, + filter::FocusedBlock, notification::{Notification, NotificationLevel}, }; -use ratatui::{ - crossterm::{ - self, - event::{KeyCode, KeyEvent, KeyModifiers}, - }, - widgets::TableState, -}; +use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; pub fn handle_key_events( key_event: KeyEvent, app: &mut App, sender: kanal::Sender, ) -> AppResult<()> { - if app.show_packet_infos_popup { - if key_event.code == KeyCode::Esc { - app.show_packet_infos_popup = false; - } - - return Ok(()); - } - - let fuzzy = app.fuzzy.clone(); - let mut fuzzy = fuzzy.lock().unwrap(); - - if fuzzy.is_enabled() { + // Start Phase + if !app.start_sniffing { match key_event.code { - KeyCode::Esc => { - if app.focused_block == FocusedBlock::Help { - app.focused_block = FocusedBlock::Main; - return Ok(()); - } - - if app.update_filters { - app.update_filters = false; - return Ok(()); - } + KeyCode::Enter => { + if app.filter.focused_block == FocusedBlock::Apply { + app.filter + .start(sender.clone(), app.data_channel_sender.clone()); - if fuzzy.is_paused() { - if app.manuall_scroll { - app.manuall_scroll = false; - } else { - fuzzy.disable(); - } - } else { - fuzzy.pause(); + app.start_sniffing = true; } } - KeyCode::Tab => { - if app.focused_block == FocusedBlock::Help { - return Ok(()); - } - - if app.update_filters { - match &app.focused_block { - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.filter.network.state.select(Some(0)); - app.filter.transport.state.select(Some(0)); - } - - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::LinkFilter; - app.filter.link.state.select(Some(0)); - app.filter.network.state.select(None); - } - - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::TrafficDirection; - app.filter.traffic_direction.state.select(Some(0)); - app.filter.link.state.select(None); - } - - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::Start; - app.filter.traffic_direction.state.select(None); - } - - FocusedBlock::Start => { - app.focused_block = FocusedBlock::TransportFilter; - } - _ => {} - }; - - return Ok(()); - } - - match app.mode { - Mode::Packet => app.mode = Mode::Stats, - Mode::Stats => app.mode = Mode::Alerts, - Mode::Alerts => app.mode = Mode::Packet, - } + KeyCode::Esc => { + app.active_popup = None; } - KeyCode::BackTab => { - if app.start_sniffing { - if app.focused_block == FocusedBlock::Help { - return Ok(()); - } - - if app.update_filters { - match &app.focused_block { - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::Start; - app.filter.transport.state.select(None); - } - - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::TransportFilter; - app.filter.transport.state.select(Some(0)); - app.filter.network.state.select(None); - } - - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.filter.network.state.select(Some(0)); - app.filter.link.state.select(None); - } - - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::LinkFilter; - app.filter.link.state.select(Some(0)); - app.filter.traffic_direction.state.select(None); - } - - FocusedBlock::Start => { - app.focused_block = FocusedBlock::TrafficDirection; - app.filter.traffic_direction.state.select(Some(0)); - } - _ => {} - } - } - } + KeyCode::Char('?') => { + app.active_popup = Some(ActivePopup::Help); } - _ => { - if app.focused_block == FocusedBlock::Help { - return Ok(()); - } - if !fuzzy.is_paused() && !app.update_filters { - fuzzy - .filter - .handle_event(&crossterm::event::Event::Key(key_event)); - } else { - match key_event.code { - KeyCode::Char('/') => { - if !app.update_filters { - fuzzy.unpause(); - } - } - - KeyCode::Char('?') => { - app.focused_block = FocusedBlock::Help; - } - - KeyCode::Char('i') => { - if app.focused_block == FocusedBlock::Help || app.update_filters { - return Ok(()); - } - if app.packet_index.is_none() || fuzzy.packets.is_empty() { - return Ok(()); - } - - app.show_packet_infos_popup = true; - } - - KeyCode::Char('f') => { - if app.focused_block != FocusedBlock::Help - && app.start_sniffing - && !app.update_filters - { - app.update_filters = true; - app.focused_block = FocusedBlock::TransportFilter; - - app.filter.network.selected_protocols = - app.filter.network.applied_protocols.clone(); - - app.filter.transport.selected_protocols = - app.filter.transport.applied_protocols.clone(); - - app.filter.link.selected_protocols = - app.filter.link.applied_protocols.clone(); - - app.filter.traffic_direction.selected_direction = - app.filter.traffic_direction.applied_direction.clone(); - - app.filter.transport.state = TableState::default().with_selected(0); - } - } - - KeyCode::Char('j') | KeyCode::Down => { - if !app.update_filters { - if !app.manuall_scroll { - app.manuall_scroll = true; - // Record the last position. Usefull for selecting the packets to display - fuzzy.packet_end_index = fuzzy.packets.len(); - } - let i = match fuzzy.scroll_state.selected() { - Some(i) => { - if i < app.packet_window_size - 1 { - i + 1 - } else if i == app.packet_window_size - 1 - && fuzzy.packets.len() > fuzzy.packet_end_index - { - // shit the window by one - fuzzy.packet_end_index += 1; - i + 1 - } else { - i - } - } - None => fuzzy.packets.len(), - }; - - fuzzy.scroll_state.select(Some(i)); - } else { - match &app.focused_block { - FocusedBlock::NetworkFilter => { - app.filter.network.scroll_down(); - } - - FocusedBlock::TransportFilter => { - app.filter.transport.scroll_down(); - } - - FocusedBlock::LinkFilter => { - app.filter.link.scroll_down(); - } - - FocusedBlock::TrafficDirection => { - app.filter.traffic_direction.state.select(Some(1)); - } - - _ => {} - }; - } - } - KeyCode::Char('k') | KeyCode::Up => { - if !app.update_filters { - if !app.manuall_scroll { - app.manuall_scroll = true; - // Record the last position. Usefull for selecting the packets to display - fuzzy.packet_end_index = fuzzy.packets.len(); - } - let i = match fuzzy.scroll_state.selected() { - Some(i) => { - if i > 1 { - i - 1 - } else if i == 0 - && fuzzy.packet_end_index > app.packet_window_size - { - // shit the window by one - fuzzy.packet_end_index -= 1; - 0 - } else { - 0 - } - } - None => fuzzy.packets.len(), - }; - - fuzzy.scroll_state.select(Some(i)); - } else { - match &app.focused_block { - FocusedBlock::NetworkFilter => { - app.filter.network.scroll_up(); - } - - FocusedBlock::TransportFilter => { - app.filter.transport.scroll_up(); - } - - FocusedBlock::LinkFilter => { - app.filter.link.scroll_up(); - } - - FocusedBlock::TrafficDirection => { - app.filter.traffic_direction.state.select(Some(0)); - } - - FocusedBlock::Help => { - app.help.scroll_up(); - } - _ => {} - } - } - } - _ => {} - } - } - } - } - } else { - match key_event.code { KeyCode::Char('q') => { - app.filter - .traffic_direction - .terminate(TrafficDirection::Egress); - app.filter - .traffic_direction - .terminate(TrafficDirection::Ingress); - thread::sleep(Duration::from_millis(110)); app.quit(); } KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.filter - .traffic_direction - .terminate(TrafficDirection::Egress); - app.filter - .traffic_direction - .terminate(TrafficDirection::Ingress); - thread::sleep(Duration::from_millis(110)); app.quit(); } } - - KeyCode::Esc => { - if app.focused_block == FocusedBlock::Help { - if app.start_sniffing { - app.focused_block = FocusedBlock::Main - } else { - app.focused_block = app.previous_focused_block; - } - return Ok(()); - } - - if app.update_filters { - app.update_filters = false; - return Ok(()); - } - - if app.manuall_scroll { - app.manuall_scroll = false; - } - } - - KeyCode::Char('?') => { - app.focused_block = FocusedBlock::Help; - } - - KeyCode::Char('f') => { - if app.focused_block != FocusedBlock::Help - && app.start_sniffing - && !app.update_filters - { - app.update_filters = true; - - app.focused_block = FocusedBlock::TransportFilter; - - app.filter.network.selected_protocols = - app.filter.network.applied_protocols.clone(); - - app.filter.transport.selected_protocols = - app.filter.transport.applied_protocols.clone(); - - app.filter.link.selected_protocols = app.filter.link.applied_protocols.clone(); - - app.filter.traffic_direction.selected_direction = - app.filter.traffic_direction.applied_direction.clone(); - - app.filter.transport.state = TableState::default().with_selected(0); - } - } - - KeyCode::Char('s') => { - if app.focused_block == FocusedBlock::Help || app.update_filters { - return Ok(()); - } - - if app.start_sniffing { - let app_packets = app.packets.lock().unwrap(); - if app_packets.is_empty() { - Notification::send( - "There is no packets".to_string(), - NotificationLevel::Info, - sender, - )?; - } else { - match export(&app_packets) { - Ok(_) => { - Notification::send( - "Packets exported to ~/oryx/capture file".to_string(), - NotificationLevel::Info, - sender, - )?; - } - Err(e) => { - Notification::send( - e.to_string(), - NotificationLevel::Error, - sender, - )?; - } - } - } - } - } - - KeyCode::Char('/') => { - if app.focused_block == FocusedBlock::Help || app.update_filters { - return Ok(()); - } - if app.start_sniffing { - fuzzy.enable(); - fuzzy.unpause(); - } + _ => { + app.filter.handle_key_events(key_event, false); } + } + return Ok(()); + } - KeyCode::Char('i') => { - if app.focused_block == FocusedBlock::Help || app.update_filters { - return Ok(()); - } - let packets = app.packets.lock().unwrap(); - - if app.packet_index.is_none() || packets.is_empty() { - return Ok(()); - } - - app.show_packet_infos_popup = true; - } + // Sniff Phase - KeyCode::Char('r') => { - if app.focused_block == FocusedBlock::Help || app.update_filters { - return Ok(()); - } - if key_event.modifiers == KeyModifiers::CONTROL { - app.filter - .traffic_direction - .terminate(TrafficDirection::Ingress); - app.filter - .traffic_direction - .terminate(TrafficDirection::Egress); - thread::sleep(Duration::from_millis(150)); - sender.send(Event::Reset)?; + if let Some(popup) = app.active_popup { + match key_event.code { + KeyCode::Esc => { + app.active_popup = None; + if popup == ActivePopup::UpdateFilters { + app.filter.handle_key_events(key_event, true); } } - KeyCode::Enter => { - if app.focused_block == FocusedBlock::Start && !app.start_sniffing { - let iface = app.interface.selected_interface.name.clone(); - - app.filter.network.apply(); - app.filter.transport.apply(); - app.filter.link.apply(); - app.filter.traffic_direction.apply(); - - if app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Ingress) - { - Ebpf::load_ingress( - iface.clone(), - sender.clone(), - app.data_channel_sender.clone(), - app.filter.ingress_channel.receiver.clone(), - app.filter.traffic_direction.terminate_ingress.clone(), - ); - } - - if app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Egress) - { - Ebpf::load_egress( - iface, - sender.clone(), - app.data_channel_sender.clone(), - app.filter.egress_channel.receiver.clone(), - app.filter.traffic_direction.terminate_egress.clone(), - ); - } - - app.start_sniffing = true; - app.focused_block = FocusedBlock::TransportFilter; - } else if app.start_sniffing && app.update_filters { - // Remove egress - if app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Egress) - && !app - .filter - .traffic_direction - .selected_direction - .contains(&TrafficDirection::Egress) - { - app.filter - .traffic_direction - .terminate(TrafficDirection::Egress); - } - - // Add egress - if !app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Egress) - && app - .filter - .traffic_direction - .selected_direction - .contains(&TrafficDirection::Egress) - { - app.filter - .traffic_direction - .terminate_egress - .store(false, std::sync::atomic::Ordering::Relaxed); - let iface = app.interface.selected_interface.name.clone(); - Ebpf::load_egress( - iface, - sender.clone(), - app.data_channel_sender.clone(), - app.filter.egress_channel.receiver.clone(), - app.filter.traffic_direction.terminate_egress.clone(), - ); - } - - // Remove ingress - if app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Ingress) - && !app - .filter - .traffic_direction - .selected_direction - .contains(&TrafficDirection::Ingress) - { - app.filter - .traffic_direction - .terminate(TrafficDirection::Ingress); - } - - // Add ingress - if !app - .filter - .traffic_direction - .applied_direction - .contains(&TrafficDirection::Ingress) - && app - .filter - .traffic_direction - .selected_direction - .contains(&TrafficDirection::Ingress) - { - let iface = app.interface.selected_interface.name.clone(); - app.filter - .traffic_direction - .terminate_ingress - .store(false, std::sync::atomic::Ordering::Relaxed); - Ebpf::load_ingress( - iface, - sender.clone(), - app.data_channel_sender.clone(), - app.filter.ingress_channel.receiver.clone(), - app.filter.traffic_direction.terminate_ingress.clone(), - ); - } - app.filter.network.apply(); - app.filter.transport.apply(); - app.filter.link.apply(); - app.filter.traffic_direction.apply(); - - thread::sleep(Duration::from_millis(150)); - app.filter - .traffic_direction - .terminate_ingress - .store(false, std::sync::atomic::Ordering::Relaxed); + if popup == ActivePopup::UpdateFilters + && app.filter.focused_block == FocusedBlock::Apply + { app.filter - .traffic_direction - .terminate_ingress - .store(false, std::sync::atomic::Ordering::Relaxed); - - app.update_filters = false; - } + .update(sender.clone(), app.data_channel_sender.clone())?; - for protocol in TransportProtocol::all().iter() { - if app.filter.transport.applied_protocols.contains(protocol) { - app.filter - .ingress_channel - .sender - .send((Protocol::Transport(*protocol), false))?; - app.filter - .egress_channel - .sender - .send((Protocol::Transport(*protocol), false))?; - } else { - app.filter - .ingress_channel - .sender - .send((Protocol::Transport(*protocol), true))?; - app.filter - .egress_channel - .sender - .send((Protocol::Transport(*protocol), true))?; - } - } - - for protocol in NetworkProtocol::all().iter() { - if app.filter.network.applied_protocols.contains(protocol) { - app.filter - .ingress_channel - .sender - .send((Protocol::Network(*protocol), false))?; - app.filter - .egress_channel - .sender - .send((Protocol::Network(*protocol), false))?; - } else { - app.filter - .ingress_channel - .sender - .send((Protocol::Network(*protocol), true))?; - app.filter - .egress_channel - .sender - .send((Protocol::Network(*protocol), true))?; - } - } - - for protocol in LinkProtocol::all().iter() { - if app.filter.link.applied_protocols.contains(protocol) { - app.filter - .ingress_channel - .sender - .send((Protocol::Link(*protocol), false))?; - app.filter - .egress_channel - .sender - .send((Protocol::Link(*protocol), false))?; - } else { - app.filter - .ingress_channel - .sender - .send((Protocol::Link(*protocol), true))?; - app.filter - .egress_channel - .sender - .send((Protocol::Link(*protocol), true))?; - } + app.active_popup = None; } } - - KeyCode::Tab => { - if app.start_sniffing { - if app.focused_block == FocusedBlock::Help { - return Ok(()); - } - - if app.update_filters { - match &app.focused_block { - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.filter.network.state.select(Some(0)); - app.filter.transport.state.select(None); - } - - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::LinkFilter; - app.filter.link.state.select(Some(0)); - app.filter.network.state.select(None); - } - - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::TrafficDirection; - app.filter.traffic_direction.state.select(Some(0)); - app.filter.link.state.select(None); - } - - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::Start; - app.filter.traffic_direction.state.select(None); - } - - FocusedBlock::Start => { - app.focused_block = FocusedBlock::TransportFilter; - app.filter.transport.state.select(Some(0)); - } - _ => {} - }; - - return Ok(()); - } - - match app.mode { - Mode::Packet => app.mode = Mode::Stats, - Mode::Stats => app.mode = Mode::Alerts, - Mode::Alerts => app.mode = Mode::Packet, - }; - } else { - match &app.focused_block { - FocusedBlock::Interface => { - app.focused_block = FocusedBlock::TransportFilter; - app.previous_focused_block = app.focused_block; - app.interface.state.select(None); - app.filter.transport.state.select(Some(0)); - } - - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.previous_focused_block = app.focused_block; - app.filter.network.state.select(Some(0)); - app.filter.transport.state.select(None); - } - - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::LinkFilter; - app.previous_focused_block = app.focused_block; - app.filter.link.state.select(Some(0)); - app.filter.network.state.select(None); - } - - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::TrafficDirection; - app.previous_focused_block = app.focused_block; - app.filter.traffic_direction.state.select(Some(0)); - app.filter.link.state.select(None); - } - - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::Start; - app.previous_focused_block = app.focused_block; - app.filter.traffic_direction.state.select(None); - } - - FocusedBlock::Start => { - app.focused_block = FocusedBlock::Interface; - app.previous_focused_block = app.focused_block; - app.interface.state.select(Some(0)); - } - _ => {} - } + _ => { + if popup == ActivePopup::UpdateFilters { + app.filter.handle_key_events(key_event, true); } } + } - KeyCode::BackTab => { - if app.start_sniffing { - if app.focused_block == FocusedBlock::Help { - return Ok(()); - } - - if app.update_filters { - match &app.focused_block { - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::Start; - app.filter.transport.state.select(None); - } - - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::TransportFilter; - app.filter.transport.state.select(Some(0)); - app.filter.network.state.select(None); - } - - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.filter.network.state.select(Some(0)); - app.filter.link.state.select(None); - } - - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::LinkFilter; - app.filter.link.state.select(Some(0)); - app.filter.traffic_direction.state.select(None); - } - - FocusedBlock::Start => { - app.focused_block = FocusedBlock::TrafficDirection; - app.filter.traffic_direction.state.select(Some(0)); - } - _ => {} - } - return Ok(()); - }; - - match app.mode { - Mode::Packet => app.mode = Mode::Alerts, - Mode::Stats => app.mode = Mode::Packet, - Mode::Alerts => app.mode = Mode::Stats, - }; - } else { - match &app.focused_block { - FocusedBlock::Interface => { - app.focused_block = FocusedBlock::Start; - app.interface.state.select(None); - } + return Ok(()); + } - FocusedBlock::TransportFilter => { - app.focused_block = FocusedBlock::Interface; - app.interface.state.select(Some(0)); - app.filter.transport.state.select(None); - } + if app.is_editing { + if key_event.code == KeyCode::Esc { + app.is_editing = false + } - FocusedBlock::NetworkFilter => { - app.focused_block = FocusedBlock::TransportFilter; - app.filter.transport.state.select(Some(0)); - app.filter.network.state.select(None); - } + app.section.handle_keys(key_event); + return Ok(()); + } - FocusedBlock::LinkFilter => { - app.focused_block = FocusedBlock::NetworkFilter; - app.filter.network.state.select(Some(0)); - app.filter.link.state.select(None); - } + match key_event.code { + KeyCode::Char('?') => { + app.active_popup = Some(ActivePopup::Help); + } - FocusedBlock::TrafficDirection => { - app.focused_block = FocusedBlock::LinkFilter; - app.filter.link.state.select(Some(0)); - app.filter.traffic_direction.state.select(None); - } + KeyCode::Char('f') => { + app.active_popup = Some(ActivePopup::UpdateFilters); + app.filter.trigger(); + } - FocusedBlock::Start => { - app.focused_block = FocusedBlock::TrafficDirection; - app.filter.traffic_direction.state.select(Some(0)); - } - _ => {} - } - } + KeyCode::Char('r') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.filter.terminate(); + thread::sleep(Duration::from_millis(150)); + sender.send(Event::Reset)?; } + } - KeyCode::Char(' ') => { - if !app.start_sniffing || app.update_filters { - match &app.focused_block { - FocusedBlock::Interface => { - if let Some(index) = app.interface.state.selected() { - let net_interface = app.interface.interfaces[index].clone(); - if net_interface.is_up { - app.interface.selected_interface = - app.interface.interfaces[index].clone(); - } - } - } - FocusedBlock::NetworkFilter => { - app.filter.network.select(); - } - - FocusedBlock::TransportFilter => { - app.filter.transport.select(); - } - - FocusedBlock::LinkFilter => { - app.filter.link.select(); - } - - FocusedBlock::TrafficDirection => { - app.filter.traffic_direction.select(); - } + KeyCode::Char('q') => { + app.filter.terminate(); + thread::sleep(Duration::from_millis(110)); + app.quit(); + } - _ => {} - } - } + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.filter.terminate(); + thread::sleep(Duration::from_millis(110)); + app.quit(); } + } - KeyCode::Char('j') | KeyCode::Down => { - if let FocusedBlock::Help = app.focused_block { - return Ok(()); - } - let app_packets = app.packets.lock().unwrap(); - // Sniff mode - if app.start_sniffing && !app.update_filters { - if !app.manuall_scroll { - app.manuall_scroll = true; - // Record the last position. Usefull for selecting the packets to display - app.packet_end_index = app_packets.len(); - } - let i = match app.packets_table_state.selected() { - Some(i) => { - if i < app.packet_window_size - 1 { - i + 1 - } else if i == app.packet_window_size - 1 - && app_packets.len() > app.packet_end_index - { - // shit the window by one - app.packet_end_index += 1; - i + 1 - } else { - i - } - } - None => app_packets.len(), - }; - - app.packets_table_state.select(Some(i)); - } else { - match &app.focused_block { - FocusedBlock::Interface => { - app.interface.scroll_down(); - } - - FocusedBlock::NetworkFilter => { - app.filter.network.scroll_down(); - } - - FocusedBlock::TransportFilter => { - app.filter.transport.scroll_down(); - } - - FocusedBlock::LinkFilter => { - app.filter.link.scroll_down(); - } - - FocusedBlock::TrafficDirection => { - app.filter.traffic_direction.state.select(Some(1)); - } + KeyCode::Char('/') => { + app.is_editing = true; + app.section.handle_keys(key_event); + } - FocusedBlock::Help => { - app.help.scroll_down(); - } - _ => {} - } - } + KeyCode::Char('i') => { + if app.section.inspection.can_show_popup() { + app.active_popup = Some(ActivePopup::PacketInfos); } + } - KeyCode::Char('k') | KeyCode::Up => { - let app_packets = app.packets.lock().unwrap(); - if let FocusedBlock::Help = app.focused_block { - return Ok(()); - } - if app.start_sniffing && !app.update_filters { - if !app.manuall_scroll { - app.manuall_scroll = true; - // Record the last position. Usefull for selecting the packets to display - app.packet_end_index = app_packets.len(); + KeyCode::Char('s') => { + let app_packets = app.packets.lock().unwrap(); + if app_packets.is_empty() { + Notification::send( + "There is no packets".to_string(), + NotificationLevel::Info, + sender, + )?; + } else { + match export(&app_packets) { + Ok(_) => { + Notification::send( + "Packets exported to ~/oryx/capture file".to_string(), + NotificationLevel::Info, + sender, + )?; } - let i = match app.packets_table_state.selected() { - Some(i) => { - if i > 1 { - i - 1 - } else if i == 0 && app.packet_end_index > app.packet_window_size { - // shit the window by one - app.packet_end_index -= 1; - 0 - } else { - 0 - } - } - None => app.packet_window_size, - }; - - app.packets_table_state.select(Some(i)); - } else { - match &app.focused_block { - FocusedBlock::Interface => { - app.interface.scroll_up(); - } - FocusedBlock::NetworkFilter => { - app.filter.network.scroll_up(); - } - - FocusedBlock::TransportFilter => { - app.filter.transport.scroll_up(); - } - - FocusedBlock::LinkFilter => { - app.filter.link.scroll_up(); - } - - FocusedBlock::TrafficDirection => { - app.filter.traffic_direction.state.select(Some(0)); - } - - FocusedBlock::Help => { - app.help.scroll_up(); - } - _ => {} + Err(e) => { + Notification::send(e.to_string(), NotificationLevel::Error, sender)?; } } } - - _ => {} + } + _ => { + app.section.handle_keys(key_event); } } diff --git a/oryx-tui/src/interface.rs b/oryx-tui/src/interface.rs index 81d6e56..808613c 100644 --- a/oryx-tui/src/interface.rs +++ b/oryx-tui/src/interface.rs @@ -14,8 +14,6 @@ use std::{ path::PathBuf, }; -use crate::app::FocusedBlock; - #[derive(Debug, Clone)] pub struct NetworkInterface { pub name: String, @@ -150,12 +148,7 @@ impl Interface { self.state.select(Some(i)); } - pub fn render_on_setup( - &mut self, - frame: &mut Frame, - block: Rect, - focused_block: &FocusedBlock, - ) { + pub fn render_on_setup(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) .constraints([ @@ -225,7 +218,7 @@ impl Interface { .title_style(Style::default().bold().fg(Color::Green)) .title_alignment(Alignment::Center) .borders(Borders::LEFT) - .border_type(if *focused_block == FocusedBlock::Interface { + .border_type(if is_focused { BorderType::Thick } else { BorderType::default() diff --git a/oryx-tui/src/lib.rs b/oryx-tui/src/lib.rs index 2534f24..2af7a70 100644 --- a/oryx-tui/src/lib.rs +++ b/oryx-tui/src/lib.rs @@ -14,31 +14,14 @@ pub mod interface; pub mod ebpf; -pub mod filters { - pub mod direction; - pub mod filter; - pub mod fuzzy; - pub mod link; - pub mod network; - pub mod transport; -} +pub mod filter; pub mod notification; pub mod export; -pub mod stats; - pub mod bandwidth; -pub mod packets { - pub mod link; - pub mod network; - pub mod packet; - pub mod transport; -} - -pub mod alerts { - pub mod alert; - pub mod syn_flood; -} +pub mod packet; + +pub mod section; diff --git a/oryx-tui/src/packets/packet.rs b/oryx-tui/src/packet.rs similarity index 97% rename from oryx-tui/src/packets/packet.rs rename to oryx-tui/src/packet.rs index 79e3cb0..543469d 100644 --- a/oryx-tui/src/packets/packet.rs +++ b/oryx-tui/src/packet.rs @@ -1,13 +1,14 @@ +pub mod link; +pub mod network; +pub mod transport; + use std::{fmt::Display, mem, net::Ipv4Addr}; +use link::{ArpPacket, ArpType, MacAddr}; +use network::{IcmpPacket, IcmpType, IpPacket, IpProto, Ipv4Packet, Ipv6Packet}; use network_types::ip::IpHdr; use oryx_common::{ProtoHdr, RawPacket}; - -use super::{ - link::{ArpPacket, ArpType, MacAddr}, - network::{IcmpPacket, IcmpType, IpPacket, IpProto, Ipv4Packet, Ipv6Packet}, - transport::{TcpPacket, UdpPacket}, -}; +use transport::{TcpPacket, UdpPacket}; #[derive(Debug, Copy, Clone)] pub enum AppPacket { diff --git a/oryx-tui/src/packets/link.rs b/oryx-tui/src/packet/link.rs similarity index 100% rename from oryx-tui/src/packets/link.rs rename to oryx-tui/src/packet/link.rs diff --git a/oryx-tui/src/packets/network.rs b/oryx-tui/src/packet/network.rs similarity index 100% rename from oryx-tui/src/packets/network.rs rename to oryx-tui/src/packet/network.rs diff --git a/oryx-tui/src/packets/transport.rs b/oryx-tui/src/packet/transport.rs similarity index 100% rename from oryx-tui/src/packets/transport.rs rename to oryx-tui/src/packet/transport.rs diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs new file mode 100644 index 0000000..3cc132a --- /dev/null +++ b/oryx-tui/src/section.rs @@ -0,0 +1,140 @@ +pub mod alert; +pub mod inspection; +pub mod stats; + +use std::sync::{Arc, Mutex}; + +use alert::Alert; +use crossterm::event::{KeyCode, KeyEvent}; + +use inspection::Inspection; +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Borders, Padding}, + Frame, +}; +use stats::Stats; + +use crate::packet::AppPacket; + +#[derive(Debug, PartialEq)] +pub enum FocusedSection { + Inspection, + Stats, + Alerts, +} + +#[derive(Debug)] +pub struct Section { + focused_section: FocusedSection, + pub inspection: Inspection, + pub stats: Stats, + pub alert: Alert, +} + +impl Section { + pub fn new(packets: Arc>>) -> Self { + Self { + focused_section: FocusedSection::Inspection, + inspection: Inspection::new(packets.clone()), + stats: Stats::new(packets.clone()), + alert: Alert::new(packets.clone()), + } + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect, network_interace: &str) { + match self.focused_section { + FocusedSection::Inspection => { + frame.render_widget( + Block::default() + .title({ + Line::from(vec![ + Span::styled( + " Inspection ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ), + Span::from(" Stats ").fg(Color::DarkGray), + self.alert.title_span(false), + ]) + }) + .title_alignment(Alignment::Left) + .padding(Padding::top(1)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + block, + ); + self.inspection.render(frame, block); + } + FocusedSection::Stats => { + frame.render_widget( + Block::default() + .title({ + Line::from(vec![ + Span::from(" Inspection ").fg(Color::DarkGray), + Span::styled( + " Stats ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ), + self.alert.title_span(false), + ]) + }) + .title_alignment(Alignment::Left) + .padding(Padding::top(1)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + block, + ); + self.stats.render(frame, block, network_interace) + } + FocusedSection::Alerts => { + frame.render_widget( + Block::default() + .title({ + Line::from(vec![ + Span::from(" Inspection ").fg(Color::DarkGray), + Span::from(" Stats ").fg(Color::DarkGray), + self.alert.title_span(true), + ]) + }) + .title_alignment(Alignment::Left) + .padding(Padding::top(1)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + block, + ); + + self.alert.render(frame, block); + } + } + } + + pub fn handle_keys(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Tab => match self.focused_section { + FocusedSection::Inspection => self.focused_section = FocusedSection::Stats, + FocusedSection::Stats => self.focused_section = FocusedSection::Alerts, + FocusedSection::Alerts => self.focused_section = FocusedSection::Inspection, + }, + + KeyCode::BackTab => match self.focused_section { + FocusedSection::Inspection => self.focused_section = FocusedSection::Alerts, + FocusedSection::Stats => self.focused_section = FocusedSection::Inspection, + FocusedSection::Alerts => self.focused_section = FocusedSection::Stats, + }, + + _ => { + if self.focused_section == FocusedSection::Inspection { + self.inspection.handle_keys(key_event); + } + } + } + } +} diff --git a/oryx-tui/src/alerts/alert.rs b/oryx-tui/src/section/alert.rs similarity index 57% rename from oryx-tui/src/alerts/alert.rs rename to oryx-tui/src/section/alert.rs index e305902..e3c5fef 100644 --- a/oryx-tui/src/alerts/alert.rs +++ b/oryx-tui/src/section/alert.rs @@ -1,15 +1,15 @@ +mod syn_flood; + use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, - text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, Padding}, + text::{Span, Text}, Frame, }; use std::sync::{atomic::Ordering, Arc, Mutex}; +use syn_flood::SynFlood; -use crate::packets::packet::AppPacket; - -use super::syn_flood::SynFlood; +use crate::packet::AppPacket; #[derive(Debug)] pub struct Alert { @@ -38,40 +38,6 @@ impl Alert { } pub fn render(&self, frame: &mut Frame, block: Rect) { - frame.render_widget( - Block::default() - .title({ - Line::from(vec![ - Span::from(" Packet ").fg(Color::DarkGray), - Span::from(" Stats ").fg(Color::DarkGray), - { - if self.detected { - if self.flash_count % 12 == 0 { - Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) - } else { - Span::from(" Alert 󰐼 ").bg(Color::Red) - } - } else { - Span::styled( - " Alert ", - Style::default().bg(Color::Green).fg(Color::White).bold(), - ) - } - }, - ]) - }) - .title_alignment(Alignment::Left) - .padding(Padding::top(1)) - .borders(Borders::ALL) - .style(Style::default()) - .border_type(BorderType::default()) - .border_style(Style::default().green()), - block.inner(Margin { - horizontal: 1, - vertical: 0, - }), - ); - if !self.detected { let text_block = Layout::default() .direction(Direction::Vertical) @@ -110,8 +76,21 @@ impl Alert { self.syn_flood.render(frame, syn_flood_block); } - pub fn title_span(&self) -> Span<'_> { - if self.detected { + pub fn title_span(&self, is_focused: bool) -> Span<'_> { + if is_focused { + if self.detected { + if self.flash_count % 12 == 0 { + Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) + } else { + Span::from(" Alert 󰐼 ").bg(Color::Red) + } + } else { + Span::styled( + " Alert ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } + } else if self.detected { if self.flash_count % 12 == 0 { Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) } else { diff --git a/oryx-tui/src/alerts/syn_flood.rs b/oryx-tui/src/section/alert/syn_flood.rs similarity index 99% rename from oryx-tui/src/alerts/syn_flood.rs rename to oryx-tui/src/section/alert/syn_flood.rs index b50c1f1..c93dfc1 100644 --- a/oryx-tui/src/alerts/syn_flood.rs +++ b/oryx-tui/src/section/alert/syn_flood.rs @@ -14,9 +14,9 @@ use ratatui::{ Frame, }; -use crate::packets::{ +use crate::packet::{ network::{IpPacket, IpProto}, - packet::AppPacket, + AppPacket, }; const WIN_SIZE: usize = 100_000; diff --git a/oryx-tui/src/section/inspection.rs b/oryx-tui/src/section/inspection.rs new file mode 100644 index 0000000..79bb9ae --- /dev/null +++ b/oryx-tui/src/section/inspection.rs @@ -0,0 +1,629 @@ +use std::sync::{Arc, Mutex}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{ + Block, BorderType, Borders, Cell, Clear, HighlightSpacing, Padding, Paragraph, Row, + Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, + }, + Frame, +}; +use tui_input::backend::crossterm::EventHandler; + +use crate::{ + filter::fuzzy::{self, Fuzzy}, + packet::{ + network::{IpPacket, IpProto}, + AppPacket, + }, +}; + +#[derive(Debug)] +pub struct Inspection { + pub packets: Arc>>, + pub state: TableState, + pub fuzzy: Arc>, + pub manuall_scroll: bool, + pub packet_end_index: usize, + pub packet_window_size: usize, + pub packet_index: Option, +} + +impl Inspection { + pub fn new(packets: Arc>>) -> Self { + Self { + packets: packets.clone(), + state: TableState::default(), + fuzzy: Fuzzy::new(packets.clone()), + manuall_scroll: false, + packet_end_index: 0, + packet_window_size: 0, + packet_index: None, + } + } + + pub fn can_show_popup(&mut self) -> bool { + let packets = self.packets.lock().unwrap(); + let fuzzy = self.fuzzy.lock().unwrap(); + + if fuzzy.is_enabled() { + !fuzzy.packets.is_empty() + } else { + !packets.is_empty() + } + } + + pub fn handle_keys(&mut self, key_event: KeyEvent) { + let fuzzy_is_enabled = { self.fuzzy.lock().unwrap().is_enabled() }; + + if fuzzy_is_enabled { + let mut fuzzy = self.fuzzy.lock().unwrap(); + match key_event.code { + KeyCode::Esc => { + if !fuzzy.is_paused() { + fuzzy.pause(); + } else if self.manuall_scroll { + self.manuall_scroll = false; + } else { + fuzzy.disable(); + } + } + _ => { + if !fuzzy.is_paused() { + fuzzy + .filter + .handle_event(&crossterm::event::Event::Key(key_event)); + } else { + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => { + if !self.manuall_scroll { + self.manuall_scroll = true; + fuzzy.packet_end_index = fuzzy.packets.len(); + } + fuzzy.scroll_down(self.packet_window_size); + } + + KeyCode::Char('/') => { + fuzzy.enable(); + fuzzy.unpause(); + } + + KeyCode::Char('k') | KeyCode::Up => { + if !self.manuall_scroll { + self.manuall_scroll = true; + fuzzy.packet_end_index = fuzzy.packets.len(); + } + fuzzy.scroll_up(self.packet_window_size); + } + + _ => {} + } + } + } + } + } else { + match key_event.code { + KeyCode::Esc => { + if self.manuall_scroll { + self.manuall_scroll = false; + } + } + + KeyCode::Char('j') | KeyCode::Down => { + self.scroll_down(); + } + + KeyCode::Char('/') => { + let mut fuzzy = self.fuzzy.lock().unwrap(); + fuzzy.enable(); + fuzzy.unpause(); + } + + KeyCode::Char('k') | KeyCode::Up => { + self.scroll_up(); + } + + _ => {} + } + } + } + + pub fn scroll_up(&mut self) { + let app_packets = self.packets.lock().unwrap(); + if !self.manuall_scroll { + self.manuall_scroll = true; + // Record the last position. Usefull for selecting the packets to display + self.packet_end_index = app_packets.len(); + } + let i = match self.state.selected() { + Some(i) => { + if i > 1 { + i - 1 + } else if i == 0 && self.packet_end_index > self.packet_window_size { + // shit the window by one + self.packet_end_index -= 1; + 0 + } else { + 0 + } + } + None => self.packet_window_size, + }; + + self.state.select(Some(i)); + } + + pub fn scroll_down(&mut self) { + let app_packets = self.packets.lock().unwrap(); + + if !self.manuall_scroll { + self.manuall_scroll = true; + self.packet_end_index = app_packets.len(); + } + let i = match self.state.selected() { + Some(i) => { + if i < self.packet_window_size - 1 { + i + 1 + } else if i == self.packet_window_size - 1 + && app_packets.len() > self.packet_end_index + { + // shit the window by one + self.packet_end_index += 1; + i + 1 + } else { + i + } + } + None => app_packets.len(), + }; + + self.state.select(Some(i)); + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect) { + let app_packets = self.packets.lock().unwrap(); + let mut fuzzy = self.fuzzy.lock().unwrap(); + let fuzzy_packets = fuzzy.clone().packets.clone(); + + let pattern = fuzzy.clone(); + let pattern = pattern.filter.value(); + + let (packet_block, fuzzy_block) = { + if fuzzy.is_enabled() { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Fill(1), + Constraint::Length(3), + ]) + .horizontal_margin(1) + .split(block); + (chunks[1], chunks[2]) + } else { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Fill(1), + Constraint::Length(1), + ]) + .horizontal_margin(1) + .split(block); + (chunks[1], chunks[2]) + } + }; + + let widths = [ + Constraint::Min(19), // Source Address + Constraint::Length(11), // Source Port + Constraint::Min(19), // Destination Address + Constraint::Length(16), // Destination Port + Constraint::Length(8), // Protocol + Constraint::Length(3), // manual scroll sign + ]; + + // The size of the window where to display packets + let window_size = block.height.saturating_sub(5) as usize; + self.packet_window_size = window_size; + + // This points always to the end of the window + if self.packet_end_index < window_size { + self.packet_end_index = window_size; + } + + if fuzzy.packet_end_index < window_size { + fuzzy.packet_end_index = window_size; + } + + let packets_to_display = match self.manuall_scroll { + true => { + if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { + if fuzzy_packets.len() > window_size { + if let Some(selected_index) = fuzzy.scroll_state.selected() { + self.packet_index = Some( + fuzzy.packet_end_index.saturating_sub(window_size) + selected_index, + ); + } + &fuzzy_packets[fuzzy.packet_end_index.saturating_sub(window_size) + ..fuzzy.packet_end_index] + } else { + if let Some(selected_index) = fuzzy.scroll_state.selected() { + self.packet_index = Some(selected_index); + } else { + self.packet_index = None; + } + &fuzzy_packets + } + } else if app_packets.len() > window_size { + if let Some(selected_index) = self.state.selected() { + self.packet_index = Some( + self.packet_end_index.saturating_sub(window_size) + selected_index, + ); + } + &app_packets + [self.packet_end_index.saturating_sub(window_size)..self.packet_end_index] + } else { + if let Some(selected_index) = self.state.selected() { + self.packet_index = Some(selected_index); + } + &app_packets + } + } + false => { + if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { + if fuzzy_packets.len() > window_size { + self.packet_index = Some(fuzzy_packets.len().saturating_sub(1)); + &fuzzy_packets[fuzzy_packets.len().saturating_sub(window_size)..] + } else { + self.packet_index = Some(fuzzy_packets.len().saturating_sub(1)); + &fuzzy_packets + } + } else if app_packets.len() > window_size { + self.packet_index = Some(app_packets.len().saturating_sub(1)); + &app_packets[app_packets.len().saturating_sub(window_size)..] + } else { + self.packet_index = Some(app_packets.len().saturating_sub(1)); + &app_packets + } + } + }; + + // Style the packets + let packets: Vec = if fuzzy.is_enabled() & !fuzzy.filter.value().is_empty() { + packets_to_display + .iter() + .map(|app_packet| match app_packet { + AppPacket::Arp(packet) => Row::new(vec![ + fuzzy::highlight(pattern, packet.src_mac.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, packet.dst_mac.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, "ARP".to_string()).cyan(), + ]), + AppPacket::Ip(packet) => match packet { + IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { + IpProto::Tcp(p) => Row::new(vec![ + fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), + fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), + fuzzy::highlight(pattern, "TCP".to_string()).cyan(), + ]), + IpProto::Udp(p) => Row::new(vec![ + fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), + fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), + fuzzy::highlight(pattern, "UDP".to_string()).cyan(), + ]), + IpProto::Icmp(_) => Row::new(vec![ + fuzzy::highlight(pattern, ipv4_packet.src_ip.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, ipv4_packet.dst_ip.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, "ICMP".to_string()).cyan(), + ]), + }, + IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { + IpProto::Tcp(p) => Row::new(vec![ + fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), + fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), + fuzzy::highlight(pattern, "TCP".to_string()).cyan(), + ]), + IpProto::Udp(p) => Row::new(vec![ + fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.src_port.to_string()).yellow(), + fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), + fuzzy::highlight(pattern, p.dst_port.to_string()).yellow(), + fuzzy::highlight(pattern, "UDP".to_string()).cyan(), + ]), + IpProto::Icmp(_) => Row::new(vec![ + fuzzy::highlight(pattern, ipv6_packet.src_ip.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, ipv6_packet.dst_ip.to_string()).blue(), + Cell::from(Line::from("-").centered()).yellow(), + fuzzy::highlight(pattern, "ICMP".to_string()).cyan(), + ]), + }, + }, + }) + .collect() + } else { + packets_to_display + .iter() + .map(|app_packet| match app_packet { + AppPacket::Arp(packet) => Row::new(vec![ + Span::from(packet.src_mac.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from(packet.dst_mac.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from("ARP".to_string()).into_centered_line().cyan(), + ]), + AppPacket::Ip(packet) => match packet { + IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { + IpProto::Tcp(p) => Row::new(vec![ + Span::from(ipv4_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.src_port.to_string()) + .into_centered_line() + .yellow(), + Span::from(ipv4_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.dst_port.to_string()) + .into_centered_line() + .yellow(), + Span::from("TCP".to_string()).into_centered_line().cyan(), + ]), + IpProto::Udp(p) => Row::new(vec![ + Span::from(ipv4_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.src_port.to_string()) + .into_centered_line() + .yellow(), + Span::from(ipv4_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.dst_port.to_string()) + .into_centered_line() + .yellow(), + Span::from("UDP".to_string()).into_centered_line().cyan(), + ]), + IpProto::Icmp(_) => Row::new(vec![ + Span::from(ipv4_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from(ipv4_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from("ICMP".to_string()).into_centered_line().cyan(), + ]), + }, + IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { + IpProto::Tcp(p) => Row::new(vec![ + Span::from(ipv6_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.src_port.to_string()) + .into_centered_line() + .yellow(), + Span::from(ipv6_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.dst_port.to_string()) + .into_centered_line() + .yellow(), + Span::from("TCP".to_string()).into_centered_line().cyan(), + ]), + IpProto::Udp(p) => Row::new(vec![ + Span::from(ipv6_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.src_port.to_string()) + .into_centered_line() + .yellow(), + Span::from(ipv6_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from(p.dst_port.to_string()) + .into_centered_line() + .yellow(), + Span::from("UDP".to_string()).into_centered_line().cyan(), + ]), + IpProto::Icmp(_) => Row::new(vec![ + Span::from(ipv6_packet.src_ip.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from(ipv6_packet.dst_ip.to_string()) + .into_centered_line() + .blue(), + Span::from("-").into_centered_line().yellow(), + Span::from("ICMP".to_string()).into_centered_line().cyan(), + ]), + }, + }, + }) + .collect() + }; + + // Always select the last packet + if !self.manuall_scroll { + if fuzzy.is_enabled() { + fuzzy.scroll_state.select(Some(packets_to_display.len())); + } else { + self.state.select(Some(packets_to_display.len())); + } + } + + let table = Table::new(packets, widths) + .header( + Row::new(vec![ + Line::from("Source Address").centered(), + Line::from("Source Port").centered(), + Line::from("Destination Address").centered(), + Line::from("Destination Port").centered(), + Line::from("Protocol").centered(), + { + if self.manuall_scroll { + Line::from("󰹆").centered().yellow() + } else { + Line::from("").centered() + } + }, + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_style(Style::new().bg(ratatui::style::Color::DarkGray)) + .highlight_spacing(HighlightSpacing::Always) + .block(Block::default().padding(Padding::uniform(1))); + + if fuzzy.is_enabled() { + frame.render_stateful_widget(table, packet_block, &mut fuzzy.scroll_state); + } else { + frame.render_stateful_widget(table, packet_block, &mut self.state); + } + + // Scrollbar + + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = if fuzzy.is_enabled() && fuzzy_packets.len() > window_size { + ScrollbarState::new(fuzzy_packets.len()).position({ + if self.manuall_scroll { + if fuzzy.packet_end_index == window_size { + 0 + } else { + fuzzy.packet_end_index + } + } else { + fuzzy.packets.len() + } + }) + } else if !fuzzy.is_enabled() && app_packets.len() > window_size { + ScrollbarState::new(app_packets.len()).position({ + if self.manuall_scroll { + if self.packet_end_index == window_size { + 0 + } else { + self.packet_end_index + } + } else { + app_packets.len() + } + }) + } else { + ScrollbarState::default() + }; + + frame.render_stateful_widget( + scrollbar, + packet_block.inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + + if fuzzy.is_enabled() { + let fuzzy = Paragraph::new(format!("> {}", fuzzy.filter.value())) + .alignment(Alignment::Left) + .style(Style::default().white()) + .block( + Block::new() + .borders(Borders::TOP) + .title(" Search  ") + .padding(Padding::horizontal(1)) + .title_style({ + if fuzzy.is_paused() { + Style::default().bold().yellow() + } else { + Style::default().bold().green() + } + }) + .border_type({ + if fuzzy.is_paused() { + BorderType::default() + } else { + BorderType::Thick + } + }) + .border_style({ + if fuzzy.is_paused() { + Style::default().yellow() + } else { + Style::default().green() + } + }), + ); + + frame.render_widget(fuzzy, fuzzy_block); + } + } + + pub fn render_packet_infos_popup(&self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(36), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Max(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let fuzzy = self.fuzzy.lock().unwrap(); + let packets = self.packets.lock().unwrap(); + + let packet = if fuzzy.is_enabled() { + fuzzy.packets[self.packet_index.unwrap()] + } else { + packets[self.packet_index.unwrap()] + }; + + frame.render_widget(Clear, block); + frame.render_widget( + Block::new() + .title(" Packet Infos 󰋼 ") + .title_style(Style::new().bold().green()) + .title_alignment(Alignment::Center) + .borders(Borders::all()) + .border_style(Style::new().green()) + .border_type(BorderType::Thick), + block, + ); + match packet { + AppPacket::Ip(ip_packet) => ip_packet.render(block, frame), + AppPacket::Arp(arp_packet) => arp_packet.render(block, frame), + }; + } +} diff --git a/oryx-tui/src/section/stats.rs b/oryx-tui/src/section/stats.rs new file mode 100644 index 0000000..a37df11 --- /dev/null +++ b/oryx-tui/src/section/stats.rs @@ -0,0 +1,335 @@ +use dns_lookup::lookup_addr; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr}, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, + style::{Color, Style}, + text::Line, + widgets::{Bar, BarChart, BarGroup, Block, Padding}, + Frame, +}; + +use crate::{ + bandwidth::Bandwidth, + packet::{ + network::{IpPacket, IpProto}, + AppPacket, + }, +}; + +#[derive(Debug, Default)] +pub struct PacketStats { + pub total: usize, + pub filtered: usize, + pub network: NetworkStats, + pub transport: TransportStats, + pub link: LinkStats, + pub addresses: HashMap, usize)>, +} + +#[derive(Debug)] +pub struct Stats { + pub packet_stats: Arc>, + pub bandwidth: Bandwidth, +} + +impl Stats { + pub fn new(packets: Arc>>) -> Self { + let packet_stats: Arc> = Arc::new(Mutex::new(PacketStats::default())); + + thread::spawn({ + let packet_stats = packet_stats.clone(); + move || { + let mut last_index = 0; + loop { + thread::sleep(Duration::from_millis(160)); + let packets = packets.lock().unwrap(); + + if packets.is_empty() { + continue; + } + let mut packet_stats = packet_stats.lock().unwrap(); + for packet in packets[last_index..].iter() { + match packet { + AppPacket::Arp(_) => { + packet_stats.link.arp += 1; + } + AppPacket::Ip(packet) => match packet { + IpPacket::V4(ipv4_packet) => { + packet_stats.network.ipv4 += 1; + + if !ipv4_packet.dst_ip.is_private() + && !ipv4_packet.dst_ip.is_loopback() + { + if let Some((_, counts)) = + packet_stats.addresses.get_mut(&ipv4_packet.dst_ip) + { + *counts += 1; + } else if let Ok(host) = + lookup_addr(&IpAddr::V4(ipv4_packet.dst_ip)) + { + packet_stats + .addresses + .insert(ipv4_packet.dst_ip, (Some(host), 1)); + } else { + packet_stats + .addresses + .insert(ipv4_packet.dst_ip, (None, 1)); + } + } + + match ipv4_packet.proto { + IpProto::Tcp(_) => { + packet_stats.transport.tcp += 1; + } + IpProto::Udp(_) => { + packet_stats.transport.udp += 1; + } + IpProto::Icmp(_) => { + packet_stats.network.icmp += 1; + } + } + } + IpPacket::V6(ipv6_packet) => { + packet_stats.network.ipv6 += 1; + match ipv6_packet.proto { + IpProto::Tcp(_) => { + packet_stats.transport.tcp += 1; + } + IpProto::Udp(_) => { + packet_stats.transport.udp += 1; + } + IpProto::Icmp(_) => { + packet_stats.network.icmp += 1; + } + } + } + }, + } + + packet_stats.total += 1; + } + + last_index = packets.len() - 1; + } + } + }); + + Self { + packet_stats, + bandwidth: Bandwidth::new(), + } + } + pub fn get_top_10( + &self, + addresses: HashMap, usize)>, + ) -> Vec<(Ipv4Addr, (Option, usize))> { + let mut items: Vec<(Ipv4Addr, (Option, usize))> = addresses.into_iter().collect(); + items.sort_by(|a, b| b.1 .1.cmp(&a.1 .1)); + items.into_iter().take(10).collect() + } + + pub fn render(&self, frame: &mut Frame, block: Rect, network_interface: &str) { + let (bandwidth_block, stats_block) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .margin(1) + .split(block); + (chunks[0], chunks[1]) + }; + + let (address_block, network_block, transport_block, link_block) = { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Max(60), + Constraint::Length(12), + Constraint::Length(20), + Constraint::Length(10), + ] + .as_ref(), + ) + .margin(1) + .flex(Flex::SpaceBetween) + .split(stats_block); + (chunks[0], chunks[1], chunks[2], chunks[3]) + }; + + let packet_stats = self.packet_stats.lock().unwrap(); + + let link_chart = BarChart::default() + .bar_width(3) + .bar_gap(1) + .data( + BarGroup::default().bars(&[Bar::default() + .label("ARP".into()) + .style(Style::new().fg(Color::LightYellow)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.link.arp * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.link.arp * 100 / packet_stats.total) as u64 + } else { + 0 + })]), + ) + .block(Block::new().padding(Padding::horizontal(1))) + .max(100); + + let transport_chart = BarChart::default() + .bar_width(4) + .bar_gap(1) + .data( + BarGroup::default().bars(&[ + Bar::default() + .label("TCP".into()) + .style(Style::new().fg(Color::LightBlue)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightBlue)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.transport.tcp * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.transport.tcp * 100 / packet_stats.total) as u64 + } else { + 0 + }), + Bar::default() + .label("UDP".into()) + .style(Style::new().fg(Color::LightGreen)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightGreen)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.transport.udp * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.transport.udp * 100 / packet_stats.total) as u64 + } else { + 0 + }), + Bar::default() + .label("ICMP".into()) + .style(Style::new().fg(Color::LightGreen)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightGreen)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.network.icmp * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.network.icmp * 100 / packet_stats.total) as u64 + } else { + 0 + }), + ]), + ) + .block(Block::new().padding(Padding::horizontal(1))) + .max(100); + + let network_chart = BarChart::default() + .bar_width(4) + .bar_gap(1) + .data( + BarGroup::default().bars(&[ + Bar::default() + .label("IPv4".into()) + .style(Style::new().fg(Color::LightRed)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightRed)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.network.ipv4 * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.network.ipv4 * 100 / packet_stats.total) as u64 + } else { + 0 + }), + Bar::default() + .label("IPv6".into()) + .style(Style::new().fg(Color::LightCyan)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightCyan)) + .text_value(if packet_stats.total != 0 { + format!("{}%", packet_stats.network.ipv6 * 100 / packet_stats.total) + } else { + "0%".to_string() + }) + .value(if packet_stats.total != 0 { + (packet_stats.network.ipv6 * 100 / packet_stats.total) as u64 + } else { + 0 + }), + ]), + ) + .block(Block::new().padding(Padding::horizontal(1))) + .max(100); + + let addresses_chart = BarChart::default() + .direction(Direction::Horizontal) + .bar_width(1) + .bar_gap(1) + .data( + BarGroup::default().bars( + &self + .get_top_10(packet_stats.addresses.clone()) + .into_iter() + .map(|(ip, (host, count))| { + Bar::default() + .label(Line::from(count.to_string())) + .style(Style::new().fg(Color::LightYellow)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) + .text_value(host.clone().unwrap_or(ip.to_string())) + .value(count as u64) + }) + .collect::>(), + ), + ) + .block( + Block::new() + .title_alignment(Alignment::Center) + .padding(Padding::horizontal(1)) + .padding(Padding::right(3)) + .title_bottom("Top visited websites (Ipv4 only)"), + ); + + frame.render_widget(addresses_chart, address_block); + frame.render_widget(transport_chart, transport_block); + frame.render_widget(network_chart, network_block); + frame.render_widget(link_chart, link_block); + + self.bandwidth + .render(frame, bandwidth_block, network_interface); + } +} + +#[derive(Debug, Default)] +pub struct NetworkStats { + pub total: usize, + pub ipv4: usize, + pub ipv6: usize, + pub icmp: usize, +} + +#[derive(Debug, Default)] +pub struct TransportStats { + pub tcp: usize, + pub udp: usize, +} + +#[derive(Debug, Default)] +pub struct LinkStats { + pub arp: usize, +} diff --git a/oryx-tui/src/stats.rs b/oryx-tui/src/stats.rs deleted file mode 100644 index 0555cc6..0000000 --- a/oryx-tui/src/stats.rs +++ /dev/null @@ -1,287 +0,0 @@ -use dns_lookup::lookup_addr; -use std::{ - collections::HashMap, - net::{IpAddr, Ipv4Addr}, -}; - -use ratatui::{ - layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, - style::{Color, Style}, - text::Line, - widgets::{Bar, BarChart, BarGroup, Block, Padding}, - Frame, -}; - -use crate::packets::{ - network::{IpPacket, IpProto}, - packet::AppPacket, -}; - -#[derive(Debug)] -pub struct Stats { - pub total: usize, - pub filtered: usize, - pub network: NetworkStats, - pub transport: TransportStats, - pub link: LinkStats, - pub addresses: HashMap, usize)>, -} - -impl Default for Stats { - fn default() -> Self { - Self::new() - } -} - -impl Stats { - pub fn new() -> Self { - Self { - total: 0, - filtered: 0, - network: NetworkStats::default(), - transport: TransportStats::default(), - link: LinkStats::default(), - addresses: HashMap::with_capacity(1024), - } - } - pub fn get_top_10(&self) -> Vec<(&Ipv4Addr, &(Option, usize))> { - let mut items: Vec<(&Ipv4Addr, &(Option, usize))> = self.addresses.iter().collect(); - items.sort_by(|a, b| b.1 .1.cmp(&a.1 .1)); - items.into_iter().take(10).collect() - } - - pub fn refresh(&mut self, packet: &AppPacket) { - match packet { - AppPacket::Arp(_) => { - self.link.arp += 1; - } - AppPacket::Ip(packet) => match packet { - IpPacket::V4(ipv4_packet) => { - self.network.ipv4 += 1; - - if !ipv4_packet.dst_ip.is_private() && !ipv4_packet.dst_ip.is_loopback() { - if let Some((_, counts)) = self.addresses.get_mut(&ipv4_packet.dst_ip) { - *counts += 1; - } else if let Ok(host) = lookup_addr(&IpAddr::V4(ipv4_packet.dst_ip)) { - self.addresses.insert(ipv4_packet.dst_ip, (Some(host), 1)); - } else { - self.addresses.insert(ipv4_packet.dst_ip, (None, 1)); - } - } - - match ipv4_packet.proto { - IpProto::Tcp(_) => { - self.transport.tcp += 1; - } - IpProto::Udp(_) => { - self.transport.udp += 1; - } - IpProto::Icmp(_) => { - self.network.icmp += 1; - } - } - } - IpPacket::V6(ipv6_packet) => { - self.network.ipv6 += 1; - match ipv6_packet.proto { - IpProto::Tcp(_) => { - self.transport.tcp += 1; - } - IpProto::Udp(_) => { - self.transport.udp += 1; - } - IpProto::Icmp(_) => { - self.network.icmp += 1; - } - } - } - }, - } - - self.total += 1; - } - - pub fn render(&self, frame: &mut Frame, stats_block: Rect) { - let (address_block, network_block, transport_block, link_block) = { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Max(60), - Constraint::Length(12), - Constraint::Length(20), - Constraint::Length(10), - ] - .as_ref(), - ) - .margin(1) - .flex(Flex::SpaceBetween) - .split(stats_block); - (chunks[0], chunks[1], chunks[2], chunks[3]) - }; - - let link_chart = BarChart::default() - .bar_width(3) - .bar_gap(1) - .data( - BarGroup::default().bars(&[Bar::default() - .label("ARP".into()) - .style(Style::new().fg(Color::LightYellow)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) - .text_value(if self.total != 0 { - format!("{}%", self.link.arp * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.link.arp * 100 / self.total) as u64 - } else { - 0 - })]), - ) - .block(Block::new().padding(Padding::horizontal(1))) - .max(100); - - let transport_chart = BarChart::default() - .bar_width(4) - .bar_gap(1) - .data( - BarGroup::default().bars(&[ - Bar::default() - .label("TCP".into()) - .style(Style::new().fg(Color::LightBlue)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightBlue)) - .text_value(if self.total != 0 { - format!("{}%", self.transport.tcp * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.transport.tcp * 100 / self.total) as u64 - } else { - 0 - }), - Bar::default() - .label("UDP".into()) - .style(Style::new().fg(Color::LightGreen)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightGreen)) - .text_value(if self.total != 0 { - format!("{}%", self.transport.udp * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.transport.udp * 100 / self.total) as u64 - } else { - 0 - }), - Bar::default() - .label("ICMP".into()) - .style(Style::new().fg(Color::LightGreen)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightGreen)) - .text_value(if self.total != 0 { - format!("{}%", self.network.icmp * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.network.icmp * 100 / self.total) as u64 - } else { - 0 - }), - ]), - ) - .block(Block::new().padding(Padding::horizontal(1))) - .max(100); - - let network_chart = BarChart::default() - .bar_width(4) - .bar_gap(1) - .data( - BarGroup::default().bars(&[ - Bar::default() - .label("IPv4".into()) - .style(Style::new().fg(Color::LightRed)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightRed)) - .text_value(if self.total != 0 { - format!("{}%", self.network.ipv4 * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.network.ipv4 * 100 / self.total) as u64 - } else { - 0 - }), - Bar::default() - .label("IPv6".into()) - .style(Style::new().fg(Color::LightCyan)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightCyan)) - .text_value(if self.total != 0 { - format!("{}%", self.network.ipv6 * 100 / self.total) - } else { - "0%".to_string() - }) - .value(if self.total != 0 { - (self.network.ipv6 * 100 / self.total) as u64 - } else { - 0 - }), - ]), - ) - .block(Block::new().padding(Padding::horizontal(1))) - .max(100); - - let addresses_chart = BarChart::default() - .direction(Direction::Horizontal) - .bar_width(1) - .bar_gap(1) - .data( - BarGroup::default().bars( - &self - .get_top_10() - .into_iter() - .map(|(ip, (host, count))| { - Bar::default() - .label(Line::from(count.to_string())) - .style(Style::new().fg(Color::LightYellow)) - .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) - .text_value(host.clone().unwrap_or(ip.to_string())) - .value(*count as u64) - }) - .collect::>(), - ), - ) - .block( - Block::new() - .title_alignment(Alignment::Center) - .padding(Padding::horizontal(1)) - .padding(Padding::right(3)) - .title_bottom("Top visited websites (Ipv4 only)"), - ); - - frame.render_widget(addresses_chart, address_block); - frame.render_widget(transport_chart, transport_block); - frame.render_widget(network_chart, network_block); - frame.render_widget(link_chart, link_block); - } -} - -#[derive(Debug, Default)] -pub struct NetworkStats { - pub total: usize, - pub ipv4: usize, - pub ipv6: usize, - pub icmp: usize, -} - -#[derive(Debug, Default)] -pub struct TransportStats { - pub tcp: usize, - pub udp: usize, -} - -#[derive(Debug, Default)] -pub struct LinkStats { - pub arp: usize, -} diff --git a/oryx-tui/src/ui.rs b/oryx-tui/src/ui.rs index cee7227..d30b9ca 100644 --- a/oryx-tui/src/ui.rs +++ b/oryx-tui/src/ui.rs @@ -1,14 +1,17 @@ use ratatui::Frame; -use crate::app::{App, FocusedBlock}; +use crate::app::{ActivePopup, App}; pub fn render(app: &mut App, frame: &mut Frame) { app.render(frame); - if let FocusedBlock::Help = app.focused_block { - app.help.render(frame); + if let Some(popup) = &app.active_popup { + match popup { + ActivePopup::Help => app.help.render(frame), + ActivePopup::PacketInfos => app.section.inspection.render_packet_infos_popup(frame), + ActivePopup::UpdateFilters => app.filter.render_update_popup(frame), + } } - for (index, notification) in app.notifications.iter().enumerate() { notification.render(index, frame); }