diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index 8906d80..a6883bb 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -28,6 +28,7 @@ pub enum ActivePopup { UpdateFilters, PacketInfos, NewFirewallRule, + NewMetricExplorer, } #[derive(Debug)] diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index dca3f05..6f8b839 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -70,6 +70,12 @@ pub fn handle_key_events( .handle_keys(key_event, event_sender.clone())?; app.is_editing = false; } + ActivePopup::NewMetricExplorer => { + app.section + .metrics + .handle_popup_keys(key_event, event_sender.clone())?; + app.is_editing = false; + } _ => {} } } @@ -92,6 +98,17 @@ pub fn handle_key_events( app.is_editing = false; } } + ActivePopup::NewMetricExplorer => { + if app + .section + .metrics + .handle_popup_keys(key_event, event_sender.clone()) + .is_ok() + { + app.active_popup = None; + app.is_editing = false; + } + } _ => {} }, @@ -104,6 +121,11 @@ pub fn handle_key_events( .firewall .handle_keys(key_event, event_sender.clone())?; } + ActivePopup::NewMetricExplorer => { + app.section + .metrics + .handle_popup_keys(key_event, event_sender.clone())?; + } _ => {} }, } @@ -158,11 +180,21 @@ pub fn handle_key_events( KeyCode::Char('n') | KeyCode::Char('e') => { if app.section.focused_section == FocusedSection::Firewall - && app.section.handle_keys(key_event, event_sender).is_ok() + && app + .section + .handle_keys(key_event, event_sender.clone()) + .is_ok() { app.is_editing = true; app.active_popup = Some(ActivePopup::NewFirewallRule); } + + if app.section.focused_section == FocusedSection::Metrics + && app.section.handle_keys(key_event, event_sender).is_ok() + { + app.is_editing = true; + app.active_popup = Some(ActivePopup::NewMetricExplorer); + } } KeyCode::Char('i') => { diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs index e9db88e..38e0f8b 100644 --- a/oryx-tui/src/section.rs +++ b/oryx-tui/src/section.rs @@ -1,6 +1,7 @@ pub mod alert; pub mod firewall; pub mod inspection; +pub mod metrics; pub mod stats; use std::sync::{Arc, Mutex}; @@ -10,6 +11,7 @@ use crossterm::event::{KeyCode, KeyEvent}; use firewall::{Firewall, FirewallSignal}; use inspection::Inspection; +use metrics::Metrics; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Style, Stylize}, @@ -30,6 +32,7 @@ use crate::{ pub enum FocusedSection { Inspection, Stats, + Metrics, Alerts, Firewall, } @@ -39,6 +42,7 @@ pub struct Section { pub focused_section: FocusedSection, pub inspection: Inspection, pub stats: Option, + pub metrics: Metrics, pub alert: Alert, pub firewall: Firewall, } @@ -52,6 +56,7 @@ impl Section { focused_section: FocusedSection::Inspection, inspection: Inspection::new(packets.clone()), stats: None, + metrics: Metrics::new(packets.clone()), alert: Alert::new(packets.clone()), firewall: Firewall::new(firewall_chans.ingress.sender, firewall_chans.egress.sender), } @@ -79,6 +84,16 @@ impl Section { Span::from(" Stats 󱕍 ").fg(Color::DarkGray) } } + FocusedSection::Metrics => { + if is_focused { + Span::styled( + " Metrics  ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } else { + Span::from(" Metrics  ").fg(Color::DarkGray) + } + } FocusedSection::Alerts => self.alert.title_span(is_focused), FocusedSection::Firewall => { if is_focused { @@ -133,6 +148,13 @@ impl Section { Span::from(" ").bold(), Span::from(": Naviguate").bold(), ]), + Some(ActivePopup::NewMetricExplorer) => Line::from(vec![ + Span::from("󱊷 ").bold(), + Span::from(": Discard").bold(), + Span::from(" | ").bold(), + Span::from("󱞦 ").bold(), + Span::from(": Run").bold(), + ]), Some(ActivePopup::PacketInfos) | Some(ActivePopup::Help) => Line::from(vec![ Span::from("󱊷 ").bold(), Span::from(": Discard Popup").bold(), @@ -191,6 +213,19 @@ impl Section { Span::from(" ").bold(), Span::from(" Nav").bold(), ]), + FocusedSection::Metrics => Line::from(vec![ + Span::from("n").bold(), + Span::from(" New").bold(), + Span::from(" | ").bold(), + Span::from("d").bold(), + Span::from(" Delete").bold(), + Span::from(" | ").bold(), + Span::from("f").bold(), + Span::from(" Filters").bold(), + Span::from(" | ").bold(), + Span::from(" ").bold(), + Span::from(" Nav").bold(), + ]), _ => Line::from(vec![ Span::from("f").bold(), Span::from(" Filters").bold(), @@ -219,6 +254,7 @@ impl Section { Line::from(vec![ self.title_span(FocusedSection::Inspection), self.title_span(FocusedSection::Stats), + self.title_span(FocusedSection::Metrics), self.title_span(FocusedSection::Alerts), self.title_span(FocusedSection::Firewall), ]) @@ -232,6 +268,7 @@ impl Section { block, ); } + pub fn render( &mut self, frame: &mut Frame, @@ -259,6 +296,7 @@ impl Section { stats.render(frame, section_block, network_interace) } } + FocusedSection::Metrics => self.metrics.render(frame, section_block), FocusedSection::Alerts => self.alert.render(frame, section_block), FocusedSection::Firewall => self.firewall.render(frame, section_block), } @@ -272,7 +310,8 @@ impl Section { 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::Stats => self.focused_section = FocusedSection::Metrics, + FocusedSection::Metrics => self.focused_section = FocusedSection::Alerts, FocusedSection::Alerts => self.focused_section = FocusedSection::Firewall, FocusedSection::Firewall => self.focused_section = FocusedSection::Inspection, }, @@ -280,7 +319,8 @@ impl Section { KeyCode::BackTab => match self.focused_section { FocusedSection::Inspection => self.focused_section = FocusedSection::Firewall, FocusedSection::Stats => self.focused_section = FocusedSection::Inspection, - FocusedSection::Alerts => self.focused_section = FocusedSection::Stats, + FocusedSection::Metrics => self.focused_section = FocusedSection::Stats, + FocusedSection::Alerts => self.focused_section = FocusedSection::Metrics, FocusedSection::Firewall => self.focused_section = FocusedSection::Alerts, }, @@ -291,6 +331,7 @@ impl Section { FocusedSection::Firewall => self .firewall .handle_keys(key_event, notification_sender.clone())?, + FocusedSection::Metrics => self.metrics.handle_keys(key_event), _ => {} }, } diff --git a/oryx-tui/src/section/metrics.rs b/oryx-tui/src/section/metrics.rs new file mode 100644 index 0000000..5758ce7 --- /dev/null +++ b/oryx-tui/src/section/metrics.rs @@ -0,0 +1,267 @@ +use std::{ + sync::{atomic::AtomicBool, Arc, Mutex}, + thread, + time::Duration, +}; + +use crossterm::event::{Event, KeyCode, KeyEvent}; +use tui_input::{backend::crossterm::EventHandler, Input}; + +use ratatui::{ + layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, + style::{Color, Style, Stylize}, + text::Line, + widgets::{ + Bar, BarChart, BarGroup, Block, Borders, Cell, Clear, HighlightSpacing, Padding, Row, Table, + }, + Frame, +}; + +use crate::{ + app::AppResult, + packet::{ + direction::TrafficDirection, + network::{IpPacket, IpProto}, + AppPacket, NetworkPacket, + }, +}; + +#[derive(Debug)] +pub struct Metrics { + user_input: Input, + app_packets: Arc>>, + port_count: Option>>, + terminate: Arc, +} + +#[derive(Debug, Default, Clone)] +pub struct PortCountMetric { + port: u16, + tcp_count: usize, + udp_count: usize, +} + +impl Metrics { + pub fn new(packets: Arc>>) -> Self { + Self { + user_input: Input::default(), + app_packets: packets, + port_count: None, + terminate: Arc::new(AtomicBool::new(false)), + } + } + + pub fn render(&self, frame: &mut Frame, block: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(4), Constraint::Fill(1)]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(block); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(90), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + if let Some(port_count_metric) = &self.port_count { + let metric = { port_count_metric.lock().unwrap().clone() }; + + let chart = BarChart::default() + .direction(Direction::Horizontal) + .bar_width(1) + .bar_gap(1) + .data( + BarGroup::default().bars(&[ + Bar::default() + .label("TCP".into()) + .style(Style::new().fg(Color::LightYellow)) + .value(metric.tcp_count.try_into().unwrap()) + .value_style(Style::new().fg(Color::Black).bg(Color::LightYellow)) + .text_value(metric.tcp_count.to_string()), + Bar::default() + .label("UDP".into()) + .style(Style::new().fg(Color::LightBlue)) + .value_style(Style::new().fg(Color::Black).bg(Color::LightBlue)) + .value(metric.udp_count.try_into().unwrap()) + .text_value(metric.udp_count.to_string()), + ]), + ) + .max((metric.udp_count + metric.tcp_count) as u64) + .block( + Block::new() + .title_alignment(Alignment::Center) + .padding(Padding::vertical(1)) + .title_top(format!("Port: {}", metric.port)), + ); + frame.render_widget(chart, block); + } + } + + pub fn handle_keys(&mut self, key_event: KeyEvent) { + if let KeyCode::Char('d') = key_event.code { + self.terminate + .store(true, std::sync::atomic::Ordering::Relaxed); + self.port_count = None; + self.user_input.reset(); + self.terminate + .store(false, std::sync::atomic::Ordering::Relaxed); + } + } + + pub fn handle_popup_keys( + &mut self, + key_event: KeyEvent, + _sender: kanal::Sender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Esc => { + self.user_input.reset(); + } + + KeyCode::Enter => { + //TODO: validate input + let port: u16 = self.user_input.value().parse().unwrap(); + let port_count = Arc::new(Mutex::new(PortCountMetric { + port, + tcp_count: 0, + udp_count: 0, + })); + + thread::spawn({ + let port_count = port_count.clone(); + let terminate = self.terminate.clone(); + let packets = self.app_packets.clone(); + move || { + let mut last_index = 0; + 'main: loop { + thread::sleep(Duration::from_millis(100)); + + let app_packets = { packets.lock().unwrap().clone() }; + + if app_packets.is_empty() { + continue; + } + let mut metric = port_count.lock().unwrap(); + for app_packet in app_packets[last_index..].iter() { + if terminate.load(std::sync::atomic::Ordering::Relaxed) { + break 'main; + } + if app_packet.direction == TrafficDirection::Ingress { + if let NetworkPacket::Ip(packet) = app_packet.packet { + match packet { + IpPacket::V4(ipv4_packet) => match ipv4_packet.proto { + IpProto::Tcp(tcp_packet) => { + if tcp_packet.dst_port == port { + metric.tcp_count += 1; + } + } + IpProto::Udp(udp_packet) => { + if udp_packet.dst_port == port { + metric.udp_count += 1; + } + } + _ => {} + }, + IpPacket::V6(ipv6_packet) => match ipv6_packet.proto { + IpProto::Tcp(tcp_packet) => { + if tcp_packet.dst_port == port { + metric.tcp_count += 1; + } + } + IpProto::Udp(udp_packet) => { + if udp_packet.dst_port == port { + metric.udp_count += 1; + } + } + _ => {} + }, + } + } + } + } + + last_index = app_packets.len(); + + if terminate.load(std::sync::atomic::Ordering::Relaxed) { + break 'main; + } + } + } + }); + + self.port_count = Some(port_count); + } + + _ => { + self.user_input.handle_event(&Event::Key(key_event)); + } + } + + Ok(()) + } + + pub fn render_new_rule_popup(&self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + //TODO: Center + let rows = [Row::new(vec![ + Cell::from("Packet Counter".to_string()) + .bg(Color::DarkGray) + .fg(Color::White), + Cell::from(self.user_input.value()) + .bg(Color::DarkGray) + .fg(Color::White), + ])]; + + let widths = [Constraint::Percentage(49), Constraint::Percentage(49)]; + + let table = Table::new(rows, widths) + .header( + Row::new(vec![ + Line::from("Metric").centered(), + Line::from("From").centered(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_spacing(HighlightSpacing::Always) + .block( + Block::default() + .title(" Metrics Explorer ") + .bold() + .title_alignment(ratatui::layout::Alignment::Center) + .borders(Borders::all()) + .border_type(ratatui::widgets::BorderType::Thick) + .border_style(Style::default().green()) + .padding(Padding::uniform(1)), + ); + + frame.render_widget(Clear, block); + frame.render_widget(table, block); + } +} diff --git a/oryx-tui/src/ui.rs b/oryx-tui/src/ui.rs index 1453a73..864b79a 100644 --- a/oryx-tui/src/ui.rs +++ b/oryx-tui/src/ui.rs @@ -11,6 +11,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { ActivePopup::PacketInfos => app.section.inspection.render_packet_infos_popup(frame), ActivePopup::UpdateFilters => app.filter.render_update_popup(frame), ActivePopup::NewFirewallRule => app.section.firewall.render_new_rule_popup(frame), + ActivePopup::NewMetricExplorer => app.section.metrics.render_new_rule_popup(frame), } } for (index, notification) in app.notifications.iter().enumerate() {