diff --git a/.gitignore b/.gitignore index b798b63..e6f5aea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +log-file debug/ target/ **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index f44dfbd..183fe81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -83,9 +92,9 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aya" @@ -112,7 +121,7 @@ checksum = "2c02024a307161cf3d1f052161958fd13b1a33e3e038083e58082c0700fdab85" dependencies = [ "bytes", "core-error", - "hashbrown", + "hashbrown 0.14.5", "log", "object", "thiserror", @@ -126,9 +135,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cassowary" @@ -147,9 +156,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.20" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bcde016d64c21da4be18b655631e5ab6d3107607e71a73a9f53eb48aae23fb" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ "shlex", ] @@ -162,9 +171,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -172,9 +181,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -184,9 +193,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -291,18 +300,18 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", @@ -312,9 +321,9 @@ dependencies = [ [[package]] name = "derive_builder_macro" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", "syn", @@ -359,6 +368,35 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -375,6 +413,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "font8x8" version = "0.3.1" @@ -383,9 +427,9 @@ checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "getrandom" @@ -408,6 +452,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "heck" version = "0.5.0" @@ -420,6 +475,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "ident_case" version = "1.0.1" @@ -475,9 +536,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libmimalloc-sys" @@ -523,11 +584,11 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.0", ] [[package]] @@ -575,9 +636,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "option-ext" @@ -601,9 +662,11 @@ dependencies = [ "crossterm", "dirs", "dns-lookup", + "env_logger", "itertools", "kanal", "libc", + "log", "mimalloc", "mio", "network-types", @@ -611,6 +674,7 @@ dependencies = [ "ratatui", "tui-big-text", "tui-input", + "uuid", ] [[package]] @@ -644,9 +708,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -683,9 +747,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags", ] @@ -701,6 +765,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustix" version = "0.38.37" @@ -820,9 +913,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -831,18 +924,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -896,9 +989,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" @@ -906,6 +999,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/Justfile b/Justfile index 0c8de83..f36f9f3 100644 --- a/Justfile +++ b/Justfile @@ -15,6 +15,10 @@ show interface: sudo tc filter show dev $interface egress # Run oryx debug +run-debug: + echo "" > log-file + RUST_LOG=info cargo xtask run 2> log-file + run: cargo xtask run @@ -29,3 +33,12 @@ build: # Profile profile: CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --root --bin oryx + +show_active_maps: + @for MAPTYPE in BLOCKLIST_IPV4 BLOCKLIST_IPV6;do \ + map_ids=$(sudo bpftool map show | grep "$MAPTYPE" | cut -f1 -d":" ); \ + for map_id in $map_ids;do \ + echo "$MAPTYPE($map_id)";\ + sudo bpftool map dump id $map_id -j | python3 showmaps.py 2>/dev/null || echo "\tempty";\ + done ;\ + done diff --git a/Readme.md b/Readme.md index 4e0fcf0..6a806ca 100644 --- a/Readme.md +++ b/Readme.md @@ -10,6 +10,7 @@ - Real-time traffic inspection and visualization. - Comprehensive Traffic Statistics. +- Firewall functionalities. - Fuzzy search. ## 💡 Prerequisites @@ -103,14 +104,26 @@ sudo oryx `f`: Update the applied filters. -`i`: Show more infos about the selected packet. - `ctrl + r`: Reset the app. `ctrl + s`: Export the capture to `~/oryx/capture` file. +#### Inspection Section + +`i`: Show more infos about the selected packet. + `/`: Start fuzzy search. +#### Firewall Section + +`Space`: Toggle firewall rules status. + +`n` : Add new firewall rule. + +`e`: Edit a firewall rule. + +`Enter`: Create or Save a firewall rule. + ## ⚖️ License GPLv3 diff --git a/Release.md b/Release.md index a5a2e52..8512aa7 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,9 @@ +## v0.4 - TBA + +### Added + +- Firewall + ## v0.3 - 2024-09-25 ### Added diff --git a/oryx-common/src/lib.rs b/oryx-common/src/lib.rs index 32483fb..36c7063 100644 --- a/oryx-common/src/lib.rs +++ b/oryx-common/src/lib.rs @@ -7,6 +7,9 @@ use network_types::{arp::ArpHdr, icmp::IcmpHdr, ip::IpHdr, tcp::TcpHdr, udp::Udp pub mod ip; pub mod protocols; +pub const MAX_FIREWALL_RULES: u32 = 32; +pub const MAX_RULES_PORT: usize = 32; + #[repr(C)] pub enum RawPacket { Ip(IpHdr, ProtoHdr), diff --git a/oryx-ebpf/src/main.rs b/oryx-ebpf/src/main.rs index f124f3d..40aa3c2 100644 --- a/oryx-ebpf/src/main.rs +++ b/oryx-ebpf/src/main.rs @@ -2,9 +2,9 @@ #![no_main] use aya_ebpf::{ - bindings::TC_ACT_PIPE, + bindings::{TC_ACT_PIPE, TC_ACT_SHOT}, macros::{classifier, map}, - maps::{Array, RingBuf}, + maps::{Array, HashMap, RingBuf}, programs::TcContext, }; use core::mem; @@ -18,7 +18,7 @@ use network_types::{ }; use oryx_common::{ protocols::{LinkProtocol, NetworkProtocol, Protocol, TransportProtocol}, - ProtoHdr, RawPacket, + ProtoHdr, RawPacket, MAX_FIREWALL_RULES, MAX_RULES_PORT, }; #[map] @@ -33,6 +33,14 @@ static TRANSPORT_FILTERS: Array = Array::with_max_entries(8, 0); #[map] static LINK_FILTERS: Array = Array::with_max_entries(8, 0); +#[map] +static BLOCKLIST_IPV6: HashMap = + HashMap::::with_max_entries(MAX_FIREWALL_RULES, 0); + +#[map] +static BLOCKLIST_IPV4: HashMap = + HashMap::::with_max_entries(MAX_FIREWALL_RULES, 0); + #[classifier] pub fn oryx(ctx: TcContext) -> i32 { match process(ctx) { @@ -61,6 +69,50 @@ fn ptr_at(ctx: &TcContext, offset: usize) -> Result<*const T, ()> { Ok((start + offset) as *const T) } +#[inline] +fn filter_for_ipv4_address( + addr: u32, + port: u16, + blocked_ports_map: &HashMap, +) -> bool { + if let Some(blocked_ports) = unsafe { blocked_ports_map.get(&addr) } { + for (idx, blocked_port) in blocked_ports.iter().enumerate() { + if *blocked_port == 0 { + if idx == 0 { + return true; + } else { + break; + } + } else if *blocked_port == port { + return true; + } + } + } + false +} + +#[inline] +fn filter_for_ipv6_address( + addr: u128, + port: u16, + blocked_ports_map: &HashMap, +) -> bool { + if let Some(blocked_ports) = unsafe { blocked_ports_map.get(&addr) } { + for (idx, blocked_port) in blocked_ports.iter().enumerate() { + if *blocked_port == 0 { + if idx == 0 { + return true; + } else { + break; + } + } else if *blocked_port == port { + return true; + } + } + } + false +} + #[inline] fn filter_packet(protocol: Protocol) -> bool { match protocol { @@ -89,26 +141,49 @@ fn process(ctx: TcContext) -> Result { match ethhdr.ether_type { EtherType::Ipv4 => { - if filter_packet(Protocol::Network(NetworkProtocol::Ipv4)) { - return Ok(TC_ACT_PIPE); - } let header: Ipv4Hdr = ctx.load(EthHdr::LEN).map_err(|_| ())?; + let src_addr = u32::from_be(header.src_addr); + let dst_addr = u32::from_be(header.dst_addr); + match header.proto { IpProto::Tcp => { - if filter_packet(Protocol::Transport(TransportProtocol::TCP)) { - return Ok(TC_ACT_PIPE); - } let tcphdr: *const TcpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; + let src_port = u16::from_be(unsafe { (*tcphdr).source }); + let dst_port = u16::from_be(unsafe { (*tcphdr).dest }); + + if filter_for_ipv4_address(src_addr, src_port, &BLOCKLIST_IPV4) + || filter_for_ipv4_address(dst_addr, dst_port, &BLOCKLIST_IPV4) + { + return Ok(TC_ACT_SHOT); + } + + if filter_packet(Protocol::Network(NetworkProtocol::Ipv4)) + || filter_packet(Protocol::Transport(TransportProtocol::TCP)) + { + return Ok(TC_ACT_PIPE); //DONT FWD PACKET TO TUI + } submit(RawPacket::Ip( IpHdr::V4(header), ProtoHdr::Tcp(unsafe { *tcphdr }), )); } IpProto::Udp => { - if filter_packet(Protocol::Transport(TransportProtocol::UDP)) { + let udphdr: *const UdpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; + let src_port = u16::from_be(unsafe { (*udphdr).source }); + let dst_port = u16::from_be(unsafe { (*udphdr).dest }); + + if filter_for_ipv4_address(src_addr, src_port, &BLOCKLIST_IPV4) + || filter_for_ipv4_address(dst_addr, dst_port, &BLOCKLIST_IPV4) + { + return Ok(TC_ACT_SHOT); + } + + if filter_packet(Protocol::Network(NetworkProtocol::Ipv4)) + || filter_packet(Protocol::Transport(TransportProtocol::UDP)) + { return Ok(TC_ACT_PIPE); } - let udphdr: *const UdpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; + submit(RawPacket::Ip( IpHdr::V4(header), ProtoHdr::Udp(unsafe { *udphdr }), @@ -128,26 +203,46 @@ fn process(ctx: TcContext) -> Result { } } EtherType::Ipv6 => { - if filter_packet(Protocol::Network(NetworkProtocol::Ipv6)) { - return Ok(TC_ACT_PIPE); - } let header: Ipv6Hdr = ctx.load(EthHdr::LEN).map_err(|_| ())?; + let src_addr = header.src_addr().to_bits(); + let dst_addr = header.dst_addr().to_bits(); + match header.next_hdr { IpProto::Tcp => { - if filter_packet(Protocol::Transport(TransportProtocol::TCP)) { - return Ok(TC_ACT_PIPE); - } let tcphdr: *const TcpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv6Hdr::LEN)?; + let src_port = u16::from_be(unsafe { (*tcphdr).source }); + let dst_port = u16::from_be(unsafe { (*tcphdr).dest }); + + if filter_for_ipv6_address(src_addr, src_port, &BLOCKLIST_IPV6) + || filter_for_ipv6_address(dst_addr, dst_port, &BLOCKLIST_IPV6) + { + return Ok(TC_ACT_SHOT); + } + if filter_packet(Protocol::Network(NetworkProtocol::Ipv6)) + || filter_packet(Protocol::Transport(TransportProtocol::TCP)) + { + return Ok(TC_ACT_PIPE); //DONT FWD PACKET TO TUI + } submit(RawPacket::Ip( IpHdr::V6(header), ProtoHdr::Tcp(unsafe { *tcphdr }), )); } IpProto::Udp => { - if filter_packet(Protocol::Transport(TransportProtocol::UDP)) { - return Ok(TC_ACT_PIPE); - } let udphdr: *const UdpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv6Hdr::LEN)?; + let src_port = u16::from_be(unsafe { (*udphdr).source }); + let dst_port = u16::from_be(unsafe { (*udphdr).dest }); + + if filter_for_ipv6_address(src_addr, src_port, &BLOCKLIST_IPV6) + || filter_for_ipv6_address(dst_addr, dst_port, &BLOCKLIST_IPV6) + { + return Ok(TC_ACT_SHOT); + } + if filter_packet(Protocol::Network(NetworkProtocol::Ipv6)) + || filter_packet(Protocol::Transport(TransportProtocol::UDP)) + { + return Ok(TC_ACT_PIPE); //DONT FWD PACKET TO TUI + } submit(RawPacket::Ip( IpHdr::V6(header), ProtoHdr::Udp(unsafe { *udphdr }), diff --git a/oryx-tui/Cargo.toml b/oryx-tui/Cargo.toml index 0c97533..8466043 100644 --- a/oryx-tui/Cargo.toml +++ b/oryx-tui/Cargo.toml @@ -24,6 +24,9 @@ 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"] } +log = "0.4" +env_logger = "0.11" [[bin]] name = "oryx" diff --git a/oryx-tui/src/app.rs b/oryx-tui/src/app.rs index ae1cc1c..cd47f60 100644 --- a/oryx-tui/src/app.rs +++ b/oryx-tui/src/app.rs @@ -22,6 +22,7 @@ pub enum ActivePopup { Help, UpdateFilters, PacketInfos, + NewFirewallRule, } #[derive(Debug)] @@ -55,6 +56,10 @@ impl App { let packets = Arc::new(Mutex::new(Vec::with_capacity(AppPacket::LEN * 1024 * 1024))); let (sender, receiver) = kanal::unbounded(); + + let (firewall_ingress_sender, firewall_ingress_receiver) = kanal::unbounded(); + let (firewall_egress_sender, firewall_egress_receiver) = kanal::unbounded(); + thread::spawn({ let packets = packets.clone(); move || loop { @@ -72,11 +77,20 @@ impl App { Self { running: true, help: Help::new(), - filter: Filter::new(), + filter: Filter::new( + firewall_ingress_sender.clone(), + firewall_ingress_receiver, + firewall_egress_sender.clone(), + firewall_egress_receiver, + ), start_sniffing: false, packets: packets.clone(), notifications: Vec::new(), - section: Section::new(packets.clone()), + section: Section::new( + packets.clone(), + firewall_ingress_sender, + firewall_egress_sender, + ), data_channel_sender: sender, is_editing: false, active_popup: None, @@ -92,9 +106,13 @@ impl App { let (settings_block, section_block) = { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(8), Constraint::Fill(1)]) + .constraints([ + Constraint::Length(6), + Constraint::Length(1), + Constraint::Fill(1), + ]) .split(frame.area()); - (chunks[0], chunks[1]) + (chunks[0], chunks[2]) }; self.section.render( diff --git a/oryx-tui/src/ebpf.rs b/oryx-tui/src/ebpf.rs index edf08c8..652a017 100644 --- a/oryx-tui/src/ebpf.rs +++ b/oryx-tui/src/ebpf.rs @@ -1,22 +1,25 @@ use std::{ io, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, os::fd::AsRawFd, sync::{atomic::AtomicBool, Arc}, - thread::{self, spawn}, + thread, time::Duration, }; use aya::{ include_bytes_aligned, - maps::{ring_buf::RingBufItem, Array, MapData, RingBuf}, + maps::{ring_buf::RingBufItem, Array, HashMap, MapData, RingBuf}, programs::{tc, SchedClassifier, TcAttachType}, Bpf, }; -use oryx_common::{protocols::Protocol, RawPacket}; +use oryx_common::{protocols::Protocol, RawPacket, MAX_RULES_PORT}; use crate::{ event::Event, + filter::FilterChannelSignal, notification::{Notification, NotificationLevel}, + section::firewall::{BlockedPort, FirewallSignal}, }; use mio::{event::Source, unix::SourceFd, Events, Interest, Poll, Registry, Token}; @@ -61,12 +64,150 @@ impl Source for RingBuffer<'_> { } } +fn update_ipv4_blocklist( + ipv4_firewall: &mut HashMap, + addr: Ipv4Addr, + port: BlockedPort, + to_insert: bool, +) { + if let Ok(mut blocked_ports) = ipv4_firewall.get(&addr.to_bits(), 0) { + match port { + BlockedPort::Single(port) => { + if to_insert { + if let Some((first_zero_index, _)) = blocked_ports + .iter() + .enumerate() + .find(|(_, &value)| value == 0) + { + blocked_ports[first_zero_index] = port; + ipv4_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } else { + unreachable!(); + } + } else { + let not_null_ports = blocked_ports + .into_iter() + .filter(|p| (*p != 0 && *p != port)) + .collect::>(); + + let mut blocked_ports = [0; MAX_RULES_PORT]; + + for (idx, p) in not_null_ports.iter().enumerate() { + blocked_ports[idx] = *p; + } + + if blocked_ports.iter().all(|&port| port == 0) { + ipv4_firewall.remove(&addr.to_bits()).unwrap(); + } else { + ipv4_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } + } + } + BlockedPort::All => { + if to_insert { + ipv4_firewall + .insert(addr.to_bits(), [0; MAX_RULES_PORT], 0) + .unwrap(); + } else { + ipv4_firewall.remove(&addr.to_bits()).unwrap(); + } + } + } + } else if to_insert { + let mut blocked_ports: [u16; MAX_RULES_PORT] = [0; MAX_RULES_PORT]; + match port { + BlockedPort::Single(port) => { + blocked_ports[0] = port; + } + BlockedPort::All => {} + } + + ipv4_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } +} + +fn update_ipv6_blocklist( + ipv6_firewall: &mut HashMap, + addr: Ipv6Addr, + port: BlockedPort, + to_insert: bool, +) { + if let Ok(mut blocked_ports) = ipv6_firewall.get(&addr.to_bits(), 0) { + match port { + BlockedPort::Single(port) => { + if to_insert { + if let Some((first_zero_index, _)) = blocked_ports + .iter() + .enumerate() + .find(|(_, &value)| value == 0) + { + blocked_ports[first_zero_index] = port; + ipv6_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } else { + //TODO: + unreachable!(); // list is full + } + } else { + let not_null_ports = blocked_ports + .into_iter() + .filter(|p| (*p != 0 && *p != port)) + .collect::>(); + + let mut blocked_ports = [0; MAX_RULES_PORT]; + + for (idx, p) in not_null_ports.iter().enumerate() { + blocked_ports[idx] = *p; + } + + if blocked_ports.iter().all(|&port| port == 0) { + ipv6_firewall.remove(&addr.to_bits()).unwrap(); + } else { + ipv6_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } + } + } + BlockedPort::All => { + if to_insert { + ipv6_firewall + .insert(addr.to_bits(), [0; MAX_RULES_PORT], 0) + .unwrap(); + } else { + ipv6_firewall.remove(&addr.to_bits()).unwrap(); + } + } + } + } else if to_insert { + let mut blocked_ports: [u16; MAX_RULES_PORT] = [0; MAX_RULES_PORT]; + match port { + BlockedPort::Single(port) => { + blocked_ports[0] = port; + } + BlockedPort::All => {} + } + + ipv6_firewall + .insert(addr.to_bits(), blocked_ports, 0) + .unwrap(); + } +} + impl Ebpf { pub fn load_ingress( iface: String, notification_sender: kanal::Sender, data_sender: kanal::Sender<[u8; RawPacket::LEN]>, - filter_channel_receiver: kanal::Receiver<(Protocol, bool)>, + filter_channel_receiver: kanal::Receiver, + firewall_ingress_receiver: kanal::Receiver, terminate: Arc, ) { thread::spawn({ @@ -147,6 +288,7 @@ impl Ebpf { let mut poll = Poll::new().unwrap(); let mut events = Events::with_capacity(128); + //filter-ebpf interface let mut transport_filters: Array<_, u32> = Array::try_from(bpf.take_map("TRANSPORT_FILTERS").unwrap()).unwrap(); @@ -156,17 +298,54 @@ impl Ebpf { let mut link_filters: Array<_, u32> = Array::try_from(bpf.take_map("LINK_FILTERS").unwrap()).unwrap(); - spawn(move || loop { - if let Ok((filter, flag)) = filter_channel_receiver.recv() { - match filter { - Protocol::Transport(p) => { - let _ = transport_filters.set(p as u32, flag as u32, 0); - } - Protocol::Network(p) => { - let _ = network_filters.set(p as u32, flag as u32, 0); + // firewall-ebpf interface + let mut ipv4_firewall: HashMap<_, u32, [u16; MAX_RULES_PORT]> = + HashMap::try_from(bpf.take_map("BLOCKLIST_IPV4").unwrap()).unwrap(); + + let mut ipv6_firewall: HashMap<_, u128, [u16; MAX_RULES_PORT]> = + HashMap::try_from(bpf.take_map("BLOCKLIST_IPV6").unwrap()).unwrap(); + + thread::spawn(move || loop { + if let Ok(signal) = firewall_ingress_receiver.recv() { + match signal { + FirewallSignal::Rule(rule) => match rule.ip { + IpAddr::V4(addr) => update_ipv4_blocklist( + &mut ipv4_firewall, + addr, + rule.port, + rule.enabled, + ), + + IpAddr::V6(addr) => update_ipv6_blocklist( + &mut ipv6_firewall, + addr, + rule.port, + rule.enabled, + ), + }, + FirewallSignal::Kill => { + break; } - Protocol::Link(p) => { - let _ = link_filters.set(p as u32, flag as u32, 0); + } + } + }); + + thread::spawn(move || loop { + if let Ok(signal) = filter_channel_receiver.recv() { + match signal { + FilterChannelSignal::Update((filter, flag)) => match filter { + Protocol::Transport(p) => { + let _ = transport_filters.set(p as u32, flag as u32, 0); + } + Protocol::Network(p) => { + let _ = network_filters.set(p as u32, flag as u32, 0); + } + Protocol::Link(p) => { + let _ = link_filters.set(p as u32, flag as u32, 0); + } + }, + FilterChannelSignal::Kill => { + break; } } } @@ -219,7 +398,8 @@ impl Ebpf { iface: String, notification_sender: kanal::Sender, data_sender: kanal::Sender<[u8; RawPacket::LEN]>, - filter_channel_receiver: kanal::Receiver<(Protocol, bool)>, + filter_channel_receiver: kanal::Receiver, + firewall_egress_receiver: kanal::Receiver, terminate: Arc, ) { thread::spawn({ @@ -296,6 +476,7 @@ impl Ebpf { let mut poll = Poll::new().unwrap(); let mut events = Events::with_capacity(128); + //filter-ebpf interface let mut transport_filters: Array<_, u32> = Array::try_from(bpf.take_map("TRANSPORT_FILTERS").unwrap()).unwrap(); @@ -305,21 +486,59 @@ impl Ebpf { let mut link_filters: Array<_, u32> = Array::try_from(bpf.take_map("LINK_FILTERS").unwrap()).unwrap(); - spawn(move || loop { - if let Ok((filter, flag)) = filter_channel_receiver.recv() { - match filter { - Protocol::Transport(p) => { - let _ = transport_filters.set(p as u32, flag as u32, 0); - } - Protocol::Network(p) => { - let _ = network_filters.set(p as u32, flag as u32, 0); + // firewall-ebpf interface + let mut ipv4_firewall: HashMap<_, u32, [u16; MAX_RULES_PORT]> = + HashMap::try_from(bpf.take_map("BLOCKLIST_IPV4").unwrap()).unwrap(); + + let mut ipv6_firewall: HashMap<_, u128, [u16; MAX_RULES_PORT]> = + HashMap::try_from(bpf.take_map("BLOCKLIST_IPV6").unwrap()).unwrap(); + + thread::spawn(move || loop { + if let Ok(signal) = firewall_egress_receiver.recv() { + match signal { + FirewallSignal::Rule(rule) => match rule.ip { + IpAddr::V4(addr) => update_ipv4_blocklist( + &mut ipv4_firewall, + addr, + rule.port, + rule.enabled, + ), + + IpAddr::V6(addr) => update_ipv6_blocklist( + &mut ipv6_firewall, + addr, + rule.port, + rule.enabled, + ), + }, + FirewallSignal::Kill => { + break; } - Protocol::Link(p) => { - let _ = link_filters.set(p as u32, flag as u32, 0); + } + } + }); + + thread::spawn(move || loop { + if let Ok(signal) = filter_channel_receiver.recv() { + match signal { + FilterChannelSignal::Update((filter, flag)) => match filter { + Protocol::Transport(p) => { + let _ = transport_filters.set(p as u32, flag as u32, 0); + } + Protocol::Network(p) => { + let _ = network_filters.set(p as u32, flag as u32, 0); + } + Protocol::Link(p) => { + let _ = link_filters.set(p as u32, flag as u32, 0); + } + }, + FilterChannelSignal::Kill => { + break; } } } }); + let mut ring_buf = RingBuffer::new(&mut bpf); poll.registry() diff --git a/oryx-tui/src/filter.rs b/oryx-tui/src/filter.rs index d6c9ea6..cba3f3b 100644 --- a/oryx-tui/src/filter.rs +++ b/oryx-tui/src/filter.rs @@ -27,22 +27,52 @@ use ratatui::{ use transport::TransportFilter; use tui_big_text::{BigText, PixelSize}; -use crate::{app::AppResult, ebpf::Ebpf, event::Event, interface::Interface}; +use crate::{ + app::AppResult, ebpf::Ebpf, event::Event, interface::Interface, + section::firewall::FirewallSignal, +}; #[derive(Debug, Clone)] -pub struct FilterChannel { - pub sender: kanal::Sender<(Protocol, bool)>, - pub receiver: kanal::Receiver<(Protocol, bool)>, +pub enum FilterChannelSignal { + Update((Protocol, bool)), + Kill, } -impl FilterChannel { +#[derive(Debug, Clone)] +pub struct Channels { + pub sender: kanal::Sender, + pub receiver: kanal::Receiver, +} + +#[derive(Debug, Clone)] +pub struct IoChans { + pub ingress: Channels, + pub egress: Channels, +} + +impl Channels { pub fn new() -> Self { let (sender, receiver) = kanal::unbounded(); Self { sender, receiver } } } -impl Default for FilterChannel { +impl IoChans { + pub fn new() -> Self { + Self { + ingress: Channels::new(), + egress: Channels::new(), + } + } +} + +impl Default for Channels { + fn default() -> Self { + Self::new() + } +} + +impl Default for IoChans { fn default() -> Self { Self::new() } @@ -65,28 +95,35 @@ pub struct Filter { pub transport: TransportFilter, pub link: LinkFilter, pub traffic_direction: TrafficDirectionFilter, - pub ingress_channel: FilterChannel, - pub egress_channel: FilterChannel, + pub filter_chans: IoChans, + pub firewall_chans: IoChans, pub focused_block: FocusedBlock, -} - -impl Default for Filter { - fn default() -> Self { - Self::new() - } + pub firewall_ingress_sender: kanal::Sender, + pub firewall_ingress_receiver: kanal::Receiver, + pub firewall_egress_sender: kanal::Sender, + pub firewall_egress_receiver: kanal::Receiver, } impl Filter { - pub fn new() -> Self { + pub fn new( + firewall_ingress_sender: kanal::Sender, + firewall_ingress_receiver: kanal::Receiver, + firewall_egress_sender: kanal::Sender, + firewall_egress_receiver: kanal::Receiver, + ) -> 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(), + filter_chans: IoChans::new(), + firewall_chans: IoChans::new(), focused_block: FocusedBlock::Interface, + firewall_ingress_sender, + firewall_ingress_receiver, + firewall_egress_sender, + firewall_egress_receiver, } } @@ -99,7 +136,7 @@ impl Filter { &mut self, notification_sender: kanal::Sender, data_sender: kanal::Sender<[u8; RawPacket::LEN]>, - ) { + ) -> AppResult<()> { let iface = self.interface.selected_interface.name.clone(); self.apply(); @@ -113,7 +150,8 @@ impl Filter { iface.clone(), notification_sender.clone(), data_sender.clone(), - self.ingress_channel.receiver.clone(), + self.filter_chans.ingress.receiver.clone(), + self.firewall_ingress_receiver.clone(), self.traffic_direction.terminate_ingress.clone(), ); } @@ -127,10 +165,15 @@ impl Filter { iface, notification_sender, data_sender, - self.egress_channel.receiver.clone(), + self.filter_chans.egress.receiver.clone(), + self.firewall_egress_receiver.clone(), self.traffic_direction.terminate_egress.clone(), ); } + + self.sync()?; + + Ok(()) } pub fn trigger(&mut self) { @@ -151,55 +194,103 @@ impl Filter { pub fn sync(&mut self) -> AppResult<()> { for protocol in TransportProtocol::all().iter() { if self.transport.applied_protocols.contains(protocol) { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Transport(*protocol), false))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Transport(*protocol), + false, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Transport(*protocol), false))?; + .send(FilterChannelSignal::Update(( + Protocol::Transport(*protocol), + false, + )))?; } else { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Transport(*protocol), true))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Transport(*protocol), + true, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Transport(*protocol), true))?; + .send(FilterChannelSignal::Update(( + Protocol::Transport(*protocol), + true, + )))?; } } for protocol in NetworkProtocol::all().iter() { if self.network.applied_protocols.contains(protocol) { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Network(*protocol), false))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Network(*protocol), + false, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Network(*protocol), false))?; + .send(FilterChannelSignal::Update(( + Protocol::Network(*protocol), + false, + )))?; } else { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Network(*protocol), true))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Network(*protocol), + true, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Network(*protocol), true))?; + .send(FilterChannelSignal::Update(( + Protocol::Network(*protocol), + true, + )))?; } } for protocol in LinkProtocol::all().iter() { if self.link.applied_protocols.contains(protocol) { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Link(*protocol), false))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Link(*protocol), + false, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Link(*protocol), false))?; + .send(FilterChannelSignal::Update(( + Protocol::Link(*protocol), + false, + )))?; } else { - self.ingress_channel + self.filter_chans + .ingress .sender - .send((Protocol::Link(*protocol), true))?; - self.egress_channel + .send(FilterChannelSignal::Update(( + Protocol::Link(*protocol), + true, + )))?; + self.filter_chans + .egress .sender - .send((Protocol::Link(*protocol), true))?; + .send(FilterChannelSignal::Update(( + Protocol::Link(*protocol), + true, + )))?; } } @@ -221,6 +312,11 @@ impl Filter { .selected_direction .contains(&TrafficDirection::Egress) { + self.firewall_egress_sender.send(FirewallSignal::Kill)?; + self.filter_chans + .egress + .sender + .send(FilterChannelSignal::Kill)?; self.traffic_direction.terminate(TrafficDirection::Egress); } @@ -244,7 +340,8 @@ impl Filter { iface, notification_sender.clone(), data_sender.clone(), - self.egress_channel.receiver.clone(), + self.filter_chans.egress.receiver.clone(), + self.firewall_egress_receiver.clone(), self.traffic_direction.terminate_egress.clone(), ); } @@ -259,6 +356,11 @@ impl Filter { .selected_direction .contains(&TrafficDirection::Ingress) { + self.firewall_ingress_sender.send(FirewallSignal::Kill)?; + self.filter_chans + .ingress + .sender + .send(FilterChannelSignal::Kill)?; self.traffic_direction.terminate(TrafficDirection::Ingress); } @@ -280,7 +382,8 @@ impl Filter { iface, notification_sender.clone(), data_sender.clone(), - self.ingress_channel.receiver.clone(), + self.filter_chans.ingress.receiver.clone(), + self.firewall_ingress_receiver.clone(), self.traffic_direction.terminate_ingress.clone(), ); } @@ -541,10 +644,14 @@ impl Filter { let (filter_summury_block, interface_block) = { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .margin(1) + .constraints([ + Constraint::Percentage(50), + Constraint::Length(1), + Constraint::Percentage(50), + ]) .split(block); - (chunks[0], chunks[1]) + + (chunks[0], chunks[2]) }; self.interface.render_on_sniffing(frame, interface_block); diff --git a/oryx-tui/src/filter/direction.rs b/oryx-tui/src/filter/direction.rs index 95104ce..0d27105 100644 --- a/oryx-tui/src/filter/direction.rs +++ b/oryx-tui/src/filter/direction.rs @@ -82,6 +82,14 @@ impl TrafficDirectionFilter { self.selected_direction.clear(); } + pub fn is_ingress_loaded(&self) -> bool { + self.applied_direction.contains(&TrafficDirection::Ingress) + } + + pub fn is_egress_loaded(&self) -> bool { + self.applied_direction.contains(&TrafficDirection::Egress) + } + pub fn render(&mut self, frame: &mut Frame, block: Rect, is_focused: bool) { let layout = Layout::default() .direction(Direction::Horizontal) diff --git a/oryx-tui/src/handler.rs b/oryx-tui/src/handler.rs index 6ffa71d..68a9fe6 100644 --- a/oryx-tui/src/handler.rs +++ b/oryx-tui/src/handler.rs @@ -6,13 +6,14 @@ use crate::{ export::export, filter::FocusedBlock, notification::{Notification, NotificationLevel}, + section::FocusedSection, }; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; pub fn handle_key_events( key_event: KeyEvent, app: &mut App, - sender: kanal::Sender, + event_sender: kanal::Sender, ) -> AppResult<()> { // Start Phase if !app.start_sniffing { @@ -20,7 +21,7 @@ pub fn handle_key_events( KeyCode::Enter => { if app.filter.focused_block == FocusedBlock::Apply { app.filter - .start(sender.clone(), app.data_channel_sender.clone()); + .start(event_sender.clone(), app.data_channel_sender.clone())?; app.start_sniffing = true; } @@ -56,36 +57,72 @@ pub fn handle_key_events( match key_event.code { KeyCode::Esc => { app.active_popup = None; - if popup == ActivePopup::UpdateFilters { - app.filter.handle_key_events(key_event, true); + match popup { + ActivePopup::UpdateFilters => { + app.filter.handle_key_events(key_event, true); + } + ActivePopup::NewFirewallRule => { + app.section + .firewall + .handle_keys(key_event, event_sender.clone())?; + app.is_editing = false; + } + _ => {} } } - KeyCode::Enter => { - if popup == ActivePopup::UpdateFilters - && app.filter.focused_block == FocusedBlock::Apply - { - app.filter - .update(sender.clone(), app.data_channel_sender.clone())?; - - app.active_popup = None; + KeyCode::Enter => match popup { + ActivePopup::UpdateFilters => { + if app.filter.focused_block == FocusedBlock::Apply { + app.filter + .update(event_sender.clone(), app.data_channel_sender.clone())?; + if !app.filter.traffic_direction.is_ingress_loaded() { + app.section.firewall.disable_ingress_rules(); + } + + if !app.filter.traffic_direction.is_egress_loaded() { + app.section.firewall.disable_egress_rules(); + } + + app.active_popup = None; + } } - } - _ => { - if popup == ActivePopup::UpdateFilters { + ActivePopup::NewFirewallRule => { + if app + .section + .firewall + .handle_keys(key_event, event_sender.clone()) + .is_ok() + { + app.active_popup = None; + app.is_editing = false; + } + } + _ => {} + }, + + _ => match popup { + ActivePopup::UpdateFilters => { app.filter.handle_key_events(key_event, true); } - } + ActivePopup::NewFirewallRule => { + app.section + .firewall + .handle_keys(key_event, event_sender.clone())?; + } + _ => {} + }, } return Ok(()); } if app.is_editing { - if key_event.code == KeyCode::Esc { - app.is_editing = false + match key_event.code { + KeyCode::Esc | KeyCode::Enter => app.is_editing = false, + _ => {} } - app.section.handle_keys(key_event); + app.section.handle_keys(key_event, event_sender.clone())?; return Ok(()); } @@ -103,7 +140,7 @@ pub fn handle_key_events( if key_event.modifiers == KeyModifiers::CONTROL { app.filter.terminate(); thread::sleep(Duration::from_millis(150)); - sender.send(Event::Reset)?; + event_sender.send(Event::Reset)?; } } @@ -122,8 +159,19 @@ pub fn handle_key_events( } KeyCode::Char('/') => { - app.is_editing = true; - app.section.handle_keys(key_event); + if app.section.focused_section == FocusedSection::Inspection { + app.is_editing = true; + app.section.handle_keys(key_event, event_sender.clone())?; + } + } + + KeyCode::Char('n') | KeyCode::Char('e') => { + if app.section.focused_section == FocusedSection::Firewall + && app.section.handle_keys(key_event, event_sender).is_ok() + { + app.is_editing = true; + app.active_popup = Some(ActivePopup::NewFirewallRule); + } } KeyCode::Char('i') => { @@ -132,13 +180,23 @@ pub fn handle_key_events( } } + KeyCode::Char(' ') => { + if app.section.focused_section == FocusedSection::Firewall { + app.section.firewall.load_rule( + event_sender.clone(), + app.filter.traffic_direction.is_ingress_loaded(), + app.filter.traffic_direction.is_egress_loaded(), + )?; + } + } + 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, + event_sender, )?; } else { match export(&app_packets) { @@ -146,17 +204,17 @@ pub fn handle_key_events( Notification::send( "Packets exported to ~/oryx/capture file".to_string(), NotificationLevel::Info, - sender, + event_sender, )?; } Err(e) => { - Notification::send(e.to_string(), NotificationLevel::Error, sender)?; + Notification::send(e.to_string(), NotificationLevel::Error, event_sender)?; } } } } _ => { - app.section.handle_keys(key_event); + app.section.handle_keys(key_event, event_sender.clone())?; } } diff --git a/oryx-tui/src/help.rs b/oryx-tui/src/help.rs index cd867ef..3685a8d 100644 --- a/oryx-tui/src/help.rs +++ b/oryx-tui/src/help.rs @@ -25,35 +25,40 @@ impl Help { state, keys: vec![ ( - Cell::from("Esc").bold().yellow(), + Cell::from("Esc").bold(), "Dismiss different pop-ups and modes", ), ( - Cell::from("Tab or Shift+Tab").bold().yellow(), + Cell::from("Tab or Shift+Tab").bold(), "Switch between different sections", ), - (Cell::from("j or Down").bold().yellow(), "Scroll down"), - (Cell::from("k or Up").bold().yellow(), "Scroll up"), - (Cell::from("?").bold().yellow(), "Show help"), - (Cell::from("q or ctrl+c").bold().yellow(), "Quit"), - (Cell::from("ctrl + r").bold().yellow(), "Reset the app"), + (Cell::from("j or Down").bold(), "Scroll down"), + (Cell::from("k or Up").bold(), "Scroll up"), + (Cell::from("?").bold(), "Show help"), + (Cell::from("q or ctrl+c").bold(), "Quit"), ( - Cell::from("Space").bold().yellow(), + Cell::from("Space").bold(), "Select/Deselect interface or filter", ), - (Cell::from("/").bold().yellow(), "Start fuzzy finding"), + (Cell::from("f").bold(), "Update the applied filters"), + (Cell::from("ctrl + r").bold(), "Reset the app"), ( - Cell::from("f").bold().yellow(), - "Update the applied filters", + Cell::from("ctrl + s").bold(), + "Export the capture to ~/oryx/capture file", ), + (Cell::from(""), ""), + (Cell::from("## Inspection").bold().yellow(), ""), ( - Cell::from("i").bold().yellow(), + Cell::from("i").bold(), "Show more infos about the selected packet", ), - ( - Cell::from("ctrl + s").bold().yellow(), - "Export the capture to ~/oryx/capture file", - ), + (Cell::from("/").bold(), "Start fuzzy finding"), + (Cell::from(""), ""), + (Cell::from("## Firewall").bold().yellow(), ""), + (Cell::from("n").bold(), "Add new firewall rule"), + (Cell::from("e").bold(), "Edit a firewall rule"), + (Cell::from("Space").bold(), "Toggle firewall rule status"), + (Cell::from("Enter").bold(), "Create or Save a firewall rule"), ], } } @@ -92,7 +97,7 @@ impl Help { .direction(Direction::Vertical) .constraints([ Constraint::Fill(1), - Constraint::Length(18), + Constraint::Length(26), Constraint::Fill(1), ]) .flex(ratatui::layout::Flex::SpaceBetween) diff --git a/oryx-tui/src/main.rs b/oryx-tui/src/main.rs index e0e5816..82a1c7d 100644 --- a/oryx-tui/src/main.rs +++ b/oryx-tui/src/main.rs @@ -13,6 +13,7 @@ use oryx_tui::{ use ratatui::{backend::CrosstermBackend, Terminal}; fn main() -> AppResult<()> { + env_logger::init(); Command::new("oryx") .about(crate_description!()) .version(crate_version!()) diff --git a/oryx-tui/src/notification.rs b/oryx-tui/src/notification.rs index 643b31f..68772ea 100644 --- a/oryx-tui/src/notification.rs +++ b/oryx-tui/src/notification.rs @@ -69,7 +69,7 @@ impl Notification { let notif = Notification { message: message.to_string(), level, - ttl: 500, + ttl: 75, }; sender.send(Event::Notification(notif))?; diff --git a/oryx-tui/src/section.rs b/oryx-tui/src/section.rs index 3cc132a..bec9732 100644 --- a/oryx-tui/src/section.rs +++ b/oryx-tui/src/section.rs @@ -1,4 +1,5 @@ pub mod alert; +pub mod firewall; pub mod inspection; pub mod stats; @@ -6,6 +7,7 @@ use std::sync::{Arc, Mutex}; use alert::Alert; use crossterm::event::{KeyCode, KeyEvent}; +use firewall::{Firewall, FirewallSignal}; use inspection::Inspection; use ratatui::{ @@ -17,124 +19,134 @@ use ratatui::{ }; use stats::Stats; -use crate::packet::AppPacket; +use crate::{app::AppResult, event::Event, packet::AppPacket}; #[derive(Debug, PartialEq)] pub enum FocusedSection { Inspection, Stats, Alerts, + Firewall, } #[derive(Debug)] pub struct Section { - focused_section: FocusedSection, + pub focused_section: FocusedSection, pub inspection: Inspection, pub stats: Stats, pub alert: Alert, + pub firewall: Firewall, } impl Section { - pub fn new(packets: Arc>>) -> Self { + pub fn new( + packets: Arc>>, + firewall_ingress_sender: kanal::Sender, + firewall_egress_sender: kanal::Sender, + ) -> Self { Self { focused_section: FocusedSection::Inspection, inspection: Inspection::new(packets.clone()), stats: Stats::new(packets.clone()), alert: Alert::new(packets.clone()), + firewall: Firewall::new(firewall_ingress_sender, firewall_egress_sender), } } - - pub fn render(&mut self, frame: &mut Frame, block: Rect, network_interace: &str) { - match self.focused_section { + fn title_span(&self, header_section: FocusedSection) -> Span { + let is_focused = self.focused_section == header_section; + match header_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); + if is_focused { + Span::styled( + " Inspection 󰏖 ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } else { + Span::from(" Inspection 󰏖 ").fg(Color::DarkGray) + } } 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) + if is_focused { + Span::styled( + " Stats 󱕍 ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } else { + Span::from(" Stats 󱕍 ").fg(Color::DarkGray) + } } - 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); + FocusedSection::Alerts => self.alert.title_span(is_focused), + FocusedSection::Firewall => { + if is_focused { + Span::styled( + " Firewall 󰞀 ", + Style::default().bg(Color::Green).fg(Color::White).bold(), + ) + } else { + Span::from(" Firewall 󰞀 ").fg(Color::DarkGray) + } } } } - pub fn handle_keys(&mut self, key_event: KeyEvent) { + pub fn render_header(&mut self, frame: &mut Frame, block: Rect) { + frame.render_widget( + Block::default() + .title({ + Line::from(vec![ + self.title_span(FocusedSection::Inspection), + self.title_span(FocusedSection::Stats), + self.title_span(FocusedSection::Alerts), + self.title_span(FocusedSection::Firewall), + ]) + }) + .title_alignment(Alignment::Left) + .padding(Padding::top(1)) + .borders(Borders::ALL) + .style(Style::default()) + .border_type(BorderType::default()) + .border_style(Style::default().green()), + block, + ); + } + pub fn render(&mut self, frame: &mut Frame, block: Rect, network_interace: &str) { + self.render_header(frame, block); + match self.focused_section { + FocusedSection::Inspection => self.inspection.render(frame, block), + FocusedSection::Stats => self.stats.render(frame, block, network_interace), + FocusedSection::Alerts => self.alert.render(frame, block), + FocusedSection::Firewall => self.firewall.render(frame, block), + } + } + + pub fn handle_keys( + &mut self, + key_event: KeyEvent, + notification_sender: kanal::Sender, + ) -> AppResult<()> { 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, + FocusedSection::Alerts => self.focused_section = FocusedSection::Firewall, + FocusedSection::Firewall => self.focused_section = FocusedSection::Inspection, }, KeyCode::BackTab => match self.focused_section { - FocusedSection::Inspection => self.focused_section = FocusedSection::Alerts, + FocusedSection::Inspection => self.focused_section = FocusedSection::Firewall, FocusedSection::Stats => self.focused_section = FocusedSection::Inspection, FocusedSection::Alerts => self.focused_section = FocusedSection::Stats, + FocusedSection::Firewall => self.focused_section = FocusedSection::Alerts, }, - _ => { - if self.focused_section == FocusedSection::Inspection { - self.inspection.handle_keys(key_event); - } - } + _ => match self.focused_section { + FocusedSection::Inspection => self.inspection.handle_keys(key_event), + FocusedSection::Firewall => self + .firewall + .handle_keys(key_event, notification_sender.clone())?, + _ => {} + }, } + Ok(()) } } diff --git a/oryx-tui/src/section/alert.rs b/oryx-tui/src/section/alert.rs index e3c5fef..7263f57 100644 --- a/oryx-tui/src/section/alert.rs +++ b/oryx-tui/src/section/alert.rs @@ -80,24 +80,24 @@ impl Alert { if is_focused { if self.detected { if self.flash_count % 12 == 0 { - Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) + Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) } else { - Span::from(" Alert 󰐼 ").bg(Color::Red) + Span::from(" Alert 󰐼 ").bg(Color::Red) } } else { Span::styled( - " Alert ", + " 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) + Span::from(" Alert 󰐼 ").fg(Color::White).bg(Color::Red) } else { - Span::from(" Alert 󰐼 ").fg(Color::Red) + Span::from(" Alert 󰐼 ").fg(Color::Red) } } else { - Span::from(" Alert ").fg(Color::DarkGray) + Span::from(" Alert 󰀦 ").fg(Color::DarkGray) } } } diff --git a/oryx-tui/src/section/firewall.rs b/oryx-tui/src/section/firewall.rs new file mode 100644 index 0000000..f6b3b43 --- /dev/null +++ b/oryx-tui/src/section/firewall.rs @@ -0,0 +1,659 @@ +use core::fmt::Display; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use oryx_common::MAX_FIREWALL_RULES; +use ratatui::{ + layout::{Constraint, Direction, Flex, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Text}, + widgets::{Block, Borders, Cell, Clear, HighlightSpacing, Padding, Row, Table, TableState}, + Frame, +}; +use std::{net::IpAddr, num::ParseIntError, str::FromStr}; +use tui_input::{backend::crossterm::EventHandler, Input}; +use uuid; + +use crate::{app::AppResult, filter::direction::TrafficDirection, notification::Notification}; + +#[derive(Debug, Clone)] +pub enum FirewallSignal { + Rule(FirewallRule), + Kill, +} + +#[derive(Debug, Clone)] +pub struct FirewallRule { + id: uuid::Uuid, + name: String, + pub enabled: bool, + pub ip: IpAddr, + pub port: BlockedPort, + direction: TrafficDirection, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BlockedPort { + Single(u16), + All, +} + +impl Display for BlockedPort { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BlockedPort::Single(p) => write!(f, "{}", p), + BlockedPort::All => write!(f, "*"), + } + } +} + +impl FromStr for BlockedPort { + type Err = ParseIntError; + fn from_str(s: &str) -> Result { + if s == "*" { + Ok(BlockedPort::All) + } else { + Ok(BlockedPort::Single(u16::from_str(s)?)) + } + } +} + +// TODO: Add direction +impl Display for FirewallRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.ip, self.port) + } +} +#[derive(Debug, Clone, PartialEq)] +pub enum FocusedInput { + Name, + Ip, + Port, + Direction, +} + +#[derive(Debug, Clone)] +struct UserInput { + id: Option, + name: UserInputField, + ip: UserInputField, + port: UserInputField, + direction: TrafficDirection, + focus_input: FocusedInput, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +impl UserInput { + pub fn new() -> Self { + Self { + id: None, + name: UserInputField::default(), + ip: UserInputField::default(), + port: UserInputField::default(), + direction: TrafficDirection::Ingress, + focus_input: FocusedInput::Name, + } + } + + fn validate_name(&mut self) { + self.name.error = None; + if self.name.field.value().is_empty() { + self.name.error = Some("Required field.".to_string()); + } + } + + fn validate_ip(&mut self) { + self.ip.error = None; + if self.ip.field.value().is_empty() { + self.ip.error = Some("Required field.".to_string()); + } else if IpAddr::from_str(self.ip.field.value()).is_err() { + self.ip.error = Some("Invalid IP Address.".to_string()); + } + } + + fn validate_port(&mut self) { + self.port.error = None; + if self.port.field.value().is_empty() { + self.port.error = Some("Required field.".to_string()); + } else if BlockedPort::from_str(self.port.field.value()).is_err() { + self.port.error = Some("Invalid Port number.".to_string()); + } + } + + fn validate(&mut self) -> AppResult<()> { + self.validate_name(); + self.validate_ip(); + self.validate_port(); + + if self.name.error.is_some() || self.ip.error.is_some() || self.port.error.is_some() { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn render(&mut 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]; + + let rows = [ + Row::new(vec![ + Cell::from(self.name.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Name { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::Black), + Cell::from(self.ip.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Ip { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::Black), + Cell::from(self.port.field.to_string()) + .bg({ + if self.focus_input == FocusedInput::Port { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::Black), + Cell::from(self.direction.to_string()) + .bg({ + if self.focus_input == FocusedInput::Direction { + Color::Gray + } else { + Color::DarkGray + } + }) + .fg(Color::Black), + ]), + Row::new(vec![ + Cell::new(""), + Cell::new(""), + Cell::new(""), + Cell::new(""), + ]), + Row::new(vec![ + Cell::from({ + if let Some(error) = &self.name.error { + error.to_string() + } else { + String::new() + } + }) + .red(), + Cell::from({ + if let Some(error) = &self.ip.error { + error.to_string() + } else { + String::new() + } + }) + .red(), + Cell::from({ + if let Some(error) = &self.port.error { + error.to_string() + } else { + String::new() + } + }) + .red(), + Cell::new(""), + ]), + ]; + + let widths = [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]; + + let table = Table::new(rows, widths) + .header( + Row::new(vec![ + Line::from("Name").centered(), + Line::from("IP").centered(), + Line::from("Port").centered(), + Line::from("Direction").centered(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_spacing(HighlightSpacing::Always) + .block( + Block::default() + .title(" Firewall Rule ") + .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); + } +} + +impl From for UserInput { + fn from(rule: FirewallRule) -> Self { + Self { + id: Some(rule.id), + name: UserInputField { + field: Input::from(rule.name), + error: None, + }, + ip: UserInputField { + field: Input::from(rule.ip.to_string()), + error: None, + }, + port: UserInputField { + field: Input::from(rule.port.to_string()), + error: None, + }, + direction: rule.direction, + focus_input: FocusedInput::Name, + } + } +} + +#[derive(Debug, Clone)] +pub struct Firewall { + rules: Vec, + state: TableState, + user_input: Option, + ingress_sender: kanal::Sender, + egress_sender: kanal::Sender, +} + +impl Firewall { + pub fn new( + ingress_sender: kanal::Sender, + egress_sender: kanal::Sender, + ) -> Self { + Self { + rules: Vec::new(), + state: TableState::default(), + user_input: None, + ingress_sender, + egress_sender, + } + } + + pub fn add_rule(&mut self) { + self.user_input = Some(UserInput::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() + && rule.direction == user_input.direction + && match user_input.id { + Some(uuid) => rule.id != uuid, + None => true, + } + }) { + let new_port = BlockedPort::from_str(user_input.port.field.value()).unwrap(); + + if exiting_rule_with_same_ip.port == new_port { + return Err("Rule validation error".into()); + } + + match exiting_rule_with_same_ip.port { + BlockedPort::Single(_) => { + if new_port == BlockedPort::All { + return Err("Rule validation error".into()); + } + } + + BlockedPort::All => { + return Err("Rule validation error".into()); + } + } + } + + Ok(()) + } + + pub fn remove_rule(&mut self, rule: &FirewallRule) { + self.rules.retain(|r| r.name != rule.name); + } + + pub fn disable_ingress_rules(&mut self) { + self.rules.iter_mut().for_each(|rule| { + if rule.enabled && rule.direction == TrafficDirection::Ingress { + rule.enabled = false; + } + }); + } + pub fn disable_egress_rules(&mut self) { + self.rules.iter_mut().for_each(|rule| { + if rule.enabled && rule.direction == TrafficDirection::Egress { + rule.enabled = false; + } + }); + } + + pub fn load_rule( + &mut self, + sender: kanal::Sender, + is_ingress_loaded: bool, + is_egress_loaded: bool, + ) -> AppResult<()> { + if let Some(index) = self.state.selected() { + let rule = &mut self.rules[index]; + + match rule.direction { + TrafficDirection::Ingress => { + if is_ingress_loaded { + rule.enabled = !rule.enabled; + self.ingress_sender + .send(FirewallSignal::Rule(rule.clone()))?; + } else { + Notification::send( + "Ingress is not loaded.", + crate::notification::NotificationLevel::Warning, + sender.clone(), + )?; + } + } + TrafficDirection::Egress => { + if is_egress_loaded { + rule.enabled = !rule.enabled; + self.egress_sender + .send(FirewallSignal::Rule(rule.clone()))?; + } else { + Notification::send( + "Egress is not loaded.", + crate::notification::NotificationLevel::Warning, + sender.clone(), + )?; + } + } + } + } + Ok(()) + } + + pub fn handle_keys( + &mut self, + key_event: KeyEvent, + sender: kanal::Sender, + ) -> AppResult<()> { + if let Some(user_input) = &mut self.user_input { + match key_event.code { + KeyCode::Esc => { + self.user_input = None; + } + + KeyCode::Enter => { + if let Some(user_input) = &mut self.user_input { + user_input.validate()?; + + if let Err(e) = Firewall::validate_duplicate_rules(&self.rules, user_input) + { + Notification::send( + "Duplicate Rule", + crate::notification::NotificationLevel::Warning, + sender.clone(), + )?; + return Err(e); + } + + if let Some(id) = user_input.id { + let rule = self.rules.iter_mut().find(|rule| rule.id == id).unwrap(); + + rule.name = user_input.name.field.to_string(); + rule.ip = IpAddr::from_str(user_input.ip.field.value()).unwrap(); + rule.port = + BlockedPort::from_str(user_input.port.field.value()).unwrap(); + rule.direction = user_input.direction; + } else { + let rule = FirewallRule { + id: uuid::Uuid::new_v4(), + name: user_input.name.field.to_string(), + ip: IpAddr::from_str(user_input.ip.field.value()).unwrap(), + port: BlockedPort::from_str(user_input.port.field.value()).unwrap(), + direction: user_input.direction, + enabled: false, + }; + self.rules.push(rule); + } + self.user_input = None; + } + } + + KeyCode::Tab => { + if let Some(user_input) = &mut self.user_input { + match user_input.focus_input { + FocusedInput::Name => user_input.focus_input = FocusedInput::Ip, + FocusedInput::Ip => user_input.focus_input = FocusedInput::Port, + FocusedInput::Port => user_input.focus_input = FocusedInput::Direction, + FocusedInput::Direction => user_input.focus_input = FocusedInput::Name, + } + } + } + + _ => match user_input.focus_input { + FocusedInput::Name => { + user_input.name.field.handle_event(&Event::Key(key_event)); + } + FocusedInput::Ip => { + user_input.ip.field.handle_event(&Event::Key(key_event)); + } + FocusedInput::Port => { + user_input.port.field.handle_event(&Event::Key(key_event)); + } + FocusedInput::Direction => match key_event.code { + KeyCode::Char('j') | KeyCode::Down => { + user_input.direction = TrafficDirection::Ingress; + } + KeyCode::Char('k') | KeyCode::Up => { + user_input.direction = TrafficDirection::Egress; + } + _ => {} + }, + }, + } + } else { + match key_event.code { + KeyCode::Char('n') => { + if self.rules.len() == MAX_FIREWALL_RULES as usize { + Notification::send( + "Max rules reached", + crate::notification::NotificationLevel::Warning, + sender.clone(), + )?; + return Err("Can not edit enabled rule".into()); + } + self.add_rule(); + } + + KeyCode::Char('e') => { + if let Some(index) = self.state.selected() { + let rule = self.rules[index].clone(); + if rule.enabled { + Notification::send( + "Can not edit enabled rule", + crate::notification::NotificationLevel::Warning, + sender.clone(), + )?; + return Err("Can not edit enabled rule".into()); + } else { + self.user_input = Some(rule.into()); + } + } + } + + KeyCode::Char('d') => { + if let Some(index) = self.state.selected() { + let rule = &mut self.rules[index]; + + rule.enabled = false; + match rule.direction { + TrafficDirection::Ingress => { + self.ingress_sender + .send(FirewallSignal::Rule(rule.clone()))?; + } + TrafficDirection::Egress => self + .egress_sender + .send(FirewallSignal::Rule(rule.clone()))?, + } + + self.rules.remove(index); + } + } + + KeyCode::Char('j') | KeyCode::Down => { + let i = match self.state.selected() { + Some(i) => { + if i < self.rules.len() - 1 { + i + 1 + } else { + i + } + } + None => 0, + }; + + self.state.select(Some(i)); + } + + KeyCode::Char('k') | KeyCode::Up => { + let i = match self.state.selected() { + Some(i) => { + if i > 1 { + i - 1 + } else { + 0 + } + } + None => 0, + }; + + self.state.select(Some(i)); + } + _ => {} + } + } + + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, block: Rect) { + if self.rules.is_empty() { + let text_block = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(3), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .margin(2) + .split(block)[1]; + + let text = Text::from("No Rules").bold().centered(); + frame.render_widget(text, text_block); + return; + } + + let widths = [ + Constraint::Max(30), + Constraint::Max(20), + Constraint::Length(10), + Constraint::Length(14), + Constraint::Length(14), + ]; + + let rows = self.rules.iter().map(|rule| { + Row::new(vec![ + Line::from(rule.name.clone()).centered().bold(), + Line::from(rule.ip.to_string()).centered().bold(), + Line::from(rule.port.to_string()).centered().bold(), + Line::from({ + match rule.direction { + TrafficDirection::Ingress => String::from("Ingress 󰁅 "), + TrafficDirection::Egress => String::from("Egress  "), + } + }) + .centered() + .bold(), + Line::from({ + if rule.enabled { + "Enabled".to_string() + } else { + "Disabled".to_string() + } + }) + .centered() + .bold(), + ]) + }); + + if self.state.selected().is_none() && !self.rules.is_empty() { + self.state.select(Some(0)); + } + + let table = Table::new(rows, widths) + .column_spacing(2) + .flex(Flex::SpaceBetween) + .highlight_style(Style::default().bg(Color::DarkGray)) + .header( + Row::new(vec![ + Line::from("Name").centered().blue(), + Line::from("IP").centered().blue(), + Line::from("Port").centered().blue(), + Line::from("Direction").centered().blue(), + Line::from("Status").centered().blue(), + ]) + .style(Style::new().bold()) + .bottom_margin(1), + ); + + frame.render_stateful_widget( + table, + block.inner(Margin { + horizontal: 2, + vertical: 2, + }), + &mut self.state, + ); + } + + pub fn render_new_rule_popup(&self, frame: &mut Frame) { + if let Some(user_input) = &mut self.user_input.clone() { + user_input.render(frame); + } + } +} diff --git a/oryx-tui/src/ui.rs b/oryx-tui/src/ui.rs index d30b9ca..1453a73 100644 --- a/oryx-tui/src/ui.rs +++ b/oryx-tui/src/ui.rs @@ -10,6 +10,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { ActivePopup::Help => app.help.render(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), } } for (index, notification) in app.notifications.iter().enumerate() { diff --git a/showmaps.py b/showmaps.py new file mode 100644 index 0000000..a9b7293 --- /dev/null +++ b/showmaps.py @@ -0,0 +1,16 @@ +import sys,json; +rules=json.loads(sys.stdin.read()) +for rule in rules: + ip_version = "ipv4" if len(rule['key'])== 4 else "ipv6" + if ip_version=="ipv6": + chunks_k = [rule['key'][idx: idx+2] for idx in range(0,len(rule['key']),2)] + k = ':'.join( reversed([f"{chunk[1]}{chunk[0]}".replace("0x","") for chunk in chunks_k])) + elif ip_version=="ipv4": + k = '.'.join( reversed([str(int(k.replace("0x",""),16)) for k in rule['key']])) + else: + raise ValueError("wrong ip version") + chunks_v =[rule['value'][idx: idx+2] for idx in range(0,len(rule['value']),2)] + ports = [int(f"{chunk[1]}{chunk[0]}".replace("0x",""),16) for chunk in chunks_v] + v = "[{}, ...]".format(', '.join([str(k) for k in ports if k!=0])) if not all(map(lambda x: x==0,ports)) else '*' + print(f"\t{k} : {v}") +