pub mod app;
pub mod education;
pub mod event;
pub mod panels;
pub mod sections;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crossterm::event::{self as ct_event, Event as CtEvent, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use crate::capture::{extract_tcp, LivePcapSource, PacketSource};
use crate::cli::Args;
use crate::error::Result;
use crate::model::connection::Direction;
use crate::model::ConnectionKey;
use crate::tracker::{unix_now_ms, ConnectionTracker};
use crate::ui::app::App;
use crate::ui::event::UiEvent;
pub fn run_live(args: &Args) -> Result<()> {
let (frame_tx, frame_rx) = mpsc::channel::<crate::capture::Frame>();
let (ui_tx, ui_rx) = mpsc::channel::<UiEvent>();
let bpf = args.bpf.clone();
let iface = args.interface.clone();
let shutdown = Arc::new(AtomicBool::new(false));
let capture_shutdown = Arc::clone(&shutdown);
let capture_handle = thread::Builder::new()
.name("seehandshake-capture".into())
.spawn(move || {
let source = match iface {
Some(name) => LivePcapSource::open(&name, &bpf),
None => LivePcapSource::open_default(&bpf),
};
let mut source = match source {
Ok(s) => s,
Err(e) => {
tracing::error!("capture: {e}");
return;
}
};
loop {
if capture_shutdown.load(Ordering::Relaxed) {
break;
}
match source.next_frame() {
Ok(Some(frame)) => {
if frame_tx.send(frame).is_err() {
break; }
}
Ok(None) => {
}
Err(e) => {
tracing::error!("capture: {e}");
break;
}
}
}
})
.map_err(|e| crate::Error::Ui(format!("failed to spawn capture thread: {e}")))?;
let tracker_ui_tx = ui_tx.clone();
let tracker_handle = thread::Builder::new()
.name("seehandshake-tracker".into())
.spawn(move || {
let mut tracker = ConnectionTracker::new();
for frame in frame_rx {
let Some(seg) = extract_tcp(&frame.bytes) else {
continue;
};
let key =
ConnectionKey::canonical(seg.src_ip, seg.src_port, seg.dst_ip, seg.dst_port);
let direction = if (seg.src_ip, seg.src_port) == (key.client_ip, key.client_port) {
Direction::ClientToServer
} else {
Direction::ServerToClient
};
let endpoint_a = std::net::SocketAddr::new(seg.src_ip, seg.src_port);
let endpoint_b = std::net::SocketAddr::new(seg.dst_ip, seg.dst_port);
let now = unix_now_ms();
match tracker.ingest(key, direction, endpoint_a, endpoint_b, seg.payload, now) {
Ok(Some(state)) => {
if tracker_ui_tx
.send(UiEvent::HandshakeUpdated(Box::new(state)))
.is_err()
{
break;
}
}
Ok(None) => {}
Err(e) => tracing::warn!("tracker: {e}"),
}
let removed = tracker.evict_stale(now);
if removed > 0 {
let _ = tracker_ui_tx.send(UiEvent::StaleEvicted(removed));
}
}
})
.map_err(|e| crate::Error::Ui(format!("failed to spawn tracker thread: {e}")))?;
enable_raw_mode().map_err(|e| crate::Error::Ui(e.to_string()))?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen).map_err(|e| crate::Error::Ui(e.to_string()))?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).map_err(|e| crate::Error::Ui(e.to_string()))?;
let outcome = run_event_loop(&mut terminal, &ui_rx);
let _ = disable_raw_mode();
let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
let _ = terminal.show_cursor();
shutdown.store(true, Ordering::Relaxed);
drop(ui_tx);
let _ = capture_handle.join();
let _ = tracker_handle.join();
outcome.map_err(|e| crate::Error::Ui(format!("running the UI event loop: {e}")))
}
fn run_event_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
ui_rx: &mpsc::Receiver<UiEvent>,
) -> Result<()> {
let mut app = App::new();
let tick = Duration::from_millis(150);
loop {
while let Ok(evt) = ui_rx.try_recv() {
app.handle_ui_event(evt);
}
terminal
.draw(|f| panels::render(f, &app))
.map_err(|e| crate::Error::Ui(e.to_string()))?;
if ct_event::poll(tick).map_err(|e| crate::Error::Ui(e.to_string()))? {
if let CtEvent::Key(key) =
ct_event::read().map_err(|e| crate::Error::Ui(e.to_string()))?
{
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('w') => app.clear_connections(),
KeyCode::Esc | KeyCode::Left => app.focus_left(),
KeyCode::Char('e') => app.toggle_education(),
KeyCode::Char('d') => app.toggle_diagram(),
KeyCode::Char('f') => app.toggle_middle_mode(),
KeyCode::Enter | KeyCode::Right => app.focus_right(),
KeyCode::Tab => app.focus_cycle(),
KeyCode::Up => app.select_prev(),
KeyCode::Down => app.select_next(),
_ => {}
}
}
}
app.tick();
}
Ok(())
}