From b7f42d930955d9f9a37551d629fbec17c0a0d823 Mon Sep 17 00:00:00 2001 From: adgaultier <105724249+adgaultier@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:12:13 +0200 Subject: [PATCH] Save/Load firewall rules (#27) Co-authored-by: Badr --- Cargo.lock | 35 +++++++++++++ Justfile | 2 +- Readme.md | 2 + Release.md | 1 + oryx-tui/Cargo.toml | 4 +- oryx-tui/src/app.rs | 5 ++ oryx-tui/src/filter/direction.rs | 3 +- oryx-tui/src/handler.rs | 25 ---------- oryx-tui/src/help.rs | 4 ++ oryx-tui/src/section.rs | 10 +++- oryx-tui/src/section/firewall.rs | 80 ++++++++++++++++++++++++++++-- oryx-tui/src/section/inspection.rs | 38 +++++++++++++- 12 files changed, 175 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd96721..20b05b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,6 +666,8 @@ dependencies = [ "network-types", "oryx-common", "ratatui", + "serde", + "serde_json", "tui-big-text", "tui-input", "uuid", @@ -819,6 +821,38 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1000,6 +1034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", + "serde", ] [[package]] diff --git a/Justfile b/Justfile index f36f9f3..a46f882 100644 --- a/Justfile +++ b/Justfile @@ -17,7 +17,7 @@ show interface: # Run oryx debug run-debug: echo "" > log-file - RUST_LOG=info cargo xtask run 2> log-file + RUST_LOG=info RUST_BACKTRACE=1 cargo xtask run 2> log-file run: cargo xtask run diff --git a/Readme.md b/Readme.md index 6a806ca..a771349 100644 --- a/Readme.md +++ b/Readme.md @@ -122,6 +122,8 @@ sudo oryx `e`: Edit a firewall rule. +`s`: Save firewall rules to `~/oryx/firewall.json` + `Enter`: Create or Save a firewall rule. ## ⚖️ License diff --git a/Release.md b/Release.md index 8512aa7..73bf84f 100644 --- a/Release.md +++ b/Release.md @@ -3,6 +3,7 @@ ### Added - Firewall +- Save and Load firewall rules. ## v0.3 - 2024-09-25 diff --git a/oryx-tui/Cargo.toml b/oryx-tui/Cargo.toml index 68513af..4826bf7 100644 --- a/oryx-tui/Cargo.toml +++ b/oryx-tui/Cargo.toml @@ -24,9 +24,11 @@ kanal = "0.1.0-pre8" mimalloc = "0.1" clap = { version = "4", features = ["derive", "cargo"] } network-types = "0.0.7" -uuid = { version = "1", default-features = false, features = ["v4"] } +uuid = { version = "1", default-features = false, features = ["v4", "serde"] } log = "0.4" env_logger = "0.11" +serde_json = "1" +serde = { version = "1", features = ["derive"] } [[bin]] name = "oryx" diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index ac3818d..e21e474 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -1,3 +1,4 @@ +use log::error; use oryx_common::RawPacket; use ratatui::{ layout::{Constraint, Direction, Layout}, @@ -134,6 +135,10 @@ impl App { } pub fn quit(&mut self) { + if let Err(e) = self.section.firewall.save_rules() { + error!("{}", e) + } + self.running = false; } } diff --git a/oryx-tui/src/filter/direction.rs b/oryx-tui/src/filter/direction.rs index 0d27105..2e5cb2d 100644 --- a/oryx-tui/src/filter/direction.rs +++ b/oryx-tui/src/filter/direction.rs @@ -12,6 +12,7 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Row, Table, TableState}, Frame, }; +use serde::{Deserialize, Serialize}; #[derive(Debug)] pub struct TrafficDirectionFilter { @@ -22,7 +23,7 @@ pub struct TrafficDirectionFilter { pub terminate_egress: Arc, } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum TrafficDirection { Ingress, Egress, diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index 68a9fe6..bc92abe 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -3,9 +3,7 @@ use std::{thread, time::Duration}; use crate::{ app::{ActivePopup, App, AppResult}, event::Event, - export::export, filter::FocusedBlock, - notification::{Notification, NotificationLevel}, section::FocusedSection, }; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -190,29 +188,6 @@ pub fn handle_key_events( } } - 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, - event_sender, - )?; - } else { - match export(&app_packets) { - Ok(_) => { - Notification::send( - "Packets exported to ~/oryx/capture file".to_string(), - NotificationLevel::Info, - event_sender, - )?; - } - Err(e) => { - Notification::send(e.to_string(), NotificationLevel::Error, event_sender)?; - } - } - } - } _ => { app.section.handle_keys(key_event, event_sender.clone())?; } diff --git a/oryx-tui/src/help.rs b/oryx-tui/src/help.rs index 3685a8d..bd68290 100644 --- a/oryx-tui/src/help.rs +++ b/oryx-tui/src/help.rs @@ -57,6 +57,10 @@ impl Help { (Cell::from("## Firewall").bold().yellow(), ""), (Cell::from("n").bold(), "Add new firewall rule"), (Cell::from("e").bold(), "Edit a firewall rule"), + ( + Cell::from("s").bold(), + "Save firewall rules to ~/oryx/firewall.json ", + ), (Cell::from("Space").bold(), "Toggle firewall rule status"), (Cell::from("Enter").bold(), "Create or Save a firewall rule"), ], diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs index 7be8994..1c080df 100644 --- a/oryx-tui/src/section.rs +++ b/oryx-tui/src/section.rs @@ -151,6 +151,9 @@ impl Section { Span::from("i").bold(), Span::from(" Infos").bold(), Span::from(" | ").bold(), + Span::from("s").bold(), + Span::from(" Save").bold(), + Span::from(" | ").bold(), Span::from("f").bold(), Span::from(" Filters").bold(), Span::from(" | ").bold(), @@ -176,6 +179,9 @@ impl Section { Span::from("e").bold(), Span::from(" Edit").bold(), Span::from(" | ").bold(), + Span::from("s").bold(), + Span::from(" Save").bold(), + Span::from(" | ").bold(), Span::from("󱁐 ").bold(), Span::from(" Toggle").bold(), Span::from(" | ").bold(), @@ -275,7 +281,9 @@ impl Section { }, _ => match self.focused_section { - FocusedSection::Inspection => self.inspection.handle_keys(key_event), + FocusedSection::Inspection => self + .inspection + .handle_keys(key_event, notification_sender.clone())?, FocusedSection::Firewall => self .firewall .handle_keys(key_event, notification_sender.clone())?, diff --git a/oryx-tui/src/section/firewall.rs b/oryx-tui/src/section/firewall.rs index 02317ef..33849d8 100644 --- a/oryx-tui/src/section/firewall.rs +++ b/oryx-tui/src/section/firewall.rs @@ -1,5 +1,6 @@ use core::fmt::Display; use crossterm::event::{Event, KeyCode, KeyEvent}; +use log::{error, info}; use oryx_common::MAX_FIREWALL_RULES; use ratatui::{ layout::{Constraint, Direction, Flex, Layout, Margin, Rect}, @@ -8,7 +9,9 @@ use ratatui::{ widgets::{Block, Borders, Cell, Clear, HighlightSpacing, Padding, Row, Table, TableState}, Frame, }; -use std::{net::IpAddr, num::ParseIntError, str::FromStr}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::{fs, net::IpAddr, num::ParseIntError, os::unix::fs::chown, str::FromStr}; use tui_input::{backend::crossterm::EventHandler, Input}; use uuid; @@ -20,7 +23,7 @@ pub enum FirewallSignal { Kill, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct FirewallRule { id: uuid::Uuid, name: String, @@ -30,7 +33,7 @@ pub struct FirewallRule { direction: TrafficDirection, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum BlockedPort { Single(u16), All, @@ -302,8 +305,16 @@ impl Firewall { ingress_sender: kanal::Sender, egress_sender: kanal::Sender, ) -> Self { + let rules_list: Vec = match Self::load_saved_rules() { + Ok(saved_rules) => saved_rules, + + Err(err) => { + error!("{}", err.to_string()); + Vec::new() + } + }; Self { - rules: Vec::new(), + rules: rules_list, state: TableState::default(), user_input: None, ingress_sender, @@ -315,6 +326,49 @@ impl Firewall { self.user_input = Some(UserInput::new()); } + pub fn save_rules(&self) -> AppResult<()> { + info!("Saving Firewall Rules"); + + let json = serde_json::to_string(&self.rules)?; + + let user_uid = unsafe { libc::geteuid() }; + + let oryx_export_dir = dirs::home_dir().unwrap().join("oryx"); + + if !oryx_export_dir.exists() { + fs::create_dir(&oryx_export_dir)?; + chown(&oryx_export_dir, Some(user_uid), Some(user_uid))?; + } + + let oryx_export_file = oryx_export_dir.join("firewall.json"); + fs::write(oryx_export_file, json)?; + info!("Firewall Rules saved"); + + Ok(()) + } + + fn load_saved_rules() -> AppResult> { + let oryx_export_file = dirs::home_dir().unwrap().join("oryx").join("firewall.json"); + if oryx_export_file.exists() { + info!("Loading Firewall Rules"); + + let json_string = fs::read_to_string(oryx_export_file)?; + + let mut parsed_rules: Vec = serde_json::from_str(&json_string)?; + + // as we don't know if ingress/egress programs are loaded we have to disable all rules + parsed_rules + .iter_mut() + .for_each(|rule| rule.enabled = false); + + info!("Firewall Rules loaded"); + Ok(parsed_rules) + } else { + info!("Firewall Rules file not found"); + Ok(Vec::new()) + } + } + fn validate_duplicate_rules(rules: &[FirewallRule], user_input: &UserInput) -> AppResult<()> { if let Some(exiting_rule_with_same_ip) = rules.iter().find(|rule| { rule.ip == IpAddr::from_str(user_input.ip.field.value()).unwrap() @@ -500,6 +554,24 @@ impl Firewall { self.add_rule(); } + KeyCode::Char('s') => match self.save_rules() { + Ok(_) => { + Notification::send( + "Firewall rules saved to ~/oryx/firewall.json file", + crate::notification::NotificationLevel::Info, + sender.clone(), + )?; + } + Err(e) => { + Notification::send( + "Error while saving firewall rules.", + crate::notification::NotificationLevel::Error, + sender.clone(), + )?; + error!("Error while saving firewall rules. {}", e); + } + }, + KeyCode::Char('e') => { if let Some(index) = self.state.selected() { let rule = self.rules[index].clone(); diff --git a/oryx-tui/src/section/inspection.rs b/oryx-tui/src/section/inspection.rs index 79bb9ae..8e02533 100644 --- a/oryx-tui/src/section/inspection.rs +++ b/oryx-tui/src/section/inspection.rs @@ -14,7 +14,10 @@ use ratatui::{ use tui_input::backend::crossterm::EventHandler; use crate::{ + app::AppResult, + export, filter::fuzzy::{self, Fuzzy}, + notification::{Notification, NotificationLevel}, packet::{ network::{IpPacket, IpProto}, AppPacket, @@ -56,7 +59,11 @@ impl Inspection { } } - pub fn handle_keys(&mut self, key_event: KeyEvent) { + pub fn handle_keys( + &mut self, + key_event: KeyEvent, + event_sender: kanal::Sender, + ) -> AppResult<()> { let fuzzy_is_enabled = { self.fuzzy.lock().unwrap().is_enabled() }; if fuzzy_is_enabled { @@ -126,9 +133,38 @@ impl Inspection { self.scroll_up(); } + KeyCode::Char('s') => { + let app_packets = self.packets.lock().unwrap(); + if app_packets.is_empty() { + Notification::send( + "There is no packets".to_string(), + NotificationLevel::Info, + event_sender, + )?; + } else { + match export::export(&app_packets) { + Ok(_) => { + Notification::send( + "Packets exported to ~/oryx/capture file".to_string(), + NotificationLevel::Info, + event_sender, + )?; + } + Err(e) => { + Notification::send( + e.to_string(), + NotificationLevel::Error, + event_sender, + )?; + } + } + } + } + _ => {} } } + Ok(()) } pub fn scroll_up(&mut self) {