commit 78c6150d59bd6eefefdf57f915c66c8d065bffb0 Author: Felix Kehrer Date: Tue Aug 17 13:40:17 2021 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2459187 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,72 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "nfq" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf3fd1239e9f2acc4d80da3ecb17f95c8714fb07cddbf64abb8bcc339ac7a4b" +dependencies = [ + "libc", +] + +[[package]] +name = "pdu" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0091978bc0658e5ea8bfd61339429ddb0489e91160bfb9a9c5b7726ccba4551" + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "zunft" +version = "0.1.0" +dependencies = [ + "lazy_static", + "nfq", + "pdu", + "regex", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..afa9ab1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +cargo-features = ["edition2021"] +[package] +name = "zunft" +version = "0.1.0" +authors = ["Felix Kehrer "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +nfq = "0.2.4" +pdu = "1.4.1" +regex = "1.5.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d347f49 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# README + +Zunft - Zauberei und nftables + +Now using the new Rust 2021 edition :) + +## Useful commands + +`cargo +nightly build --release && sudo setcap cap_net_admin+ep ./target/release/zunft && cargo +nightly run --release` +`sudo iptables -t filter -A INPUT -j NFQUEUE --queue-num 0 --queue-bypass` +`sudo iptables -L INPUT --line-numbers` +`sudo iptables -D INPUT 1` + +## Testing the filter + +While you can run whatever you want, the test case I made is for you to serve the assets folder with something like `python -m http.server 8000`, and then try to have a lookg at the files. Normally you should be able to get both files, but once the firewall runs, trying to access `/secret`should not work anymore. \ No newline at end of file diff --git a/assets/public b/assets/public new file mode 100644 index 0000000..e850aac --- /dev/null +++ b/assets/public @@ -0,0 +1,4 @@ +you should be able to read this. +if not, you probably filtered too much. +but then again, if that happens you won't be able to read this. +so, erm, good luck, i guess. \ No newline at end of file diff --git a/assets/secret b/assets/secret new file mode 100644 index 0000000..9596391 --- /dev/null +++ b/assets/secret @@ -0,0 +1,2 @@ +lol, you should not be able to see this. +is the firewall actually running? \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8a4ec80 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,233 @@ +#[allow(unused_imports)] +use lazy_static::lazy_static; +use nfq::{Queue, Verdict}; +use pdu::{Gre, GrePdu, Icmp, IcmpPdu, Ip, Ipv4, Ipv4Pdu, Ipv6Pdu, Tcp, TcpPdu, Udp, UdpPdu}; +#[allow(unused_imports)] +use regex::bytes::Regex; + +fn main() { + let mut queue = Queue::open().expect("Could not open queue!"); + queue.bind(0).expect("Could not bind to queue 0!"); + loop { + let mut msg = queue.recv().expect("Could not receive message from queue!"); + let packet = msg.get_payload(); + + let verdict = match Ip::new(packet) { + Ok(Ip::Ipv4(ipv4_pdu)) => { + print!("IPv4 -> "); + //print_ipv4(ipv4_pdu); + match ipv4_pdu.inner() { + #[allow(unused_variables)] + Ok(Ipv4::Raw(raw)) => { + print!("RAW"); + Verdict::Accept + } + Ok(Ipv4::Tcp(tcp_pdu)) => { + print!("TCP -> "); + //print_tcp(tcp_pdu); + match tcp_pdu.inner() { + Ok(Tcp::Raw(raw)) => { + print!("RAW"); + filter_ipv4_tcp_raw(ipv4_pdu, tcp_pdu, raw) + } + Err(e) => panic!("Could not get inner payload of TCP PDU: {}", e), + } + } + Ok(Ipv4::Udp(udp_pdu)) => { + print!("UDP -> "); + //print_udp(udp_pdu); + match udp_pdu.inner() { + #[allow(unused_variables)] + Ok(Udp::Raw(raw)) => { + print!("RAW"); + } + Err(e) => panic!("Could not get inner payload of UDP PDU: {}", e), + } + Verdict::Accept + } + Ok(Ipv4::Icmp(icmp_pdu)) => { + print!("ICMP -> "); + //print_icmp(icmp_pdu); + match icmp_pdu.inner() { + #[allow(unused_variables)] + Ok(Icmp::Raw(raw)) => { + print!("RAW"); + } + Err(e) => panic!("Could not get inner payload of ICMP PDU: {}", e), + } + Verdict::Accept + } + Ok(Ipv4::Gre(gre_pdu)) => { + print!("GRE -> "); + //print_gre(gre_pdu); + match gre_pdu.inner() { + #[allow(unused_variables)] + Ok(Gre::Raw(raw)) => { + print!("RAW"); + } + Ok(_) => print!("Eth/Ipv4/Ipv6"), + Err(e) => panic!("Could not get inner payload of GRE PDU: {}", e), + } + Verdict::Accept + } + Err(e) => panic!("Could not decode inner packet: {}", e), + } + } + #[allow(unused_variables)] + Ok(Ip::Ipv6(ipv6_pdu)) => { + println!("IPv6!"); + // TODO same as above + Verdict::Accept + } + Err(e) => panic!("Could not decode IP packet: {}", e), + }; + println!(); + + msg.set_verdict(verdict); + queue + .verdict(msg) + .expect("Could not set verdict for message!"); + } +} + +#[allow(dead_code)] +fn print_gre(gre_pdu: GrePdu) { + print!( + "GRE {:10} {:3} {:5} {:5} {:5} {:5} {:?} {:?} {:?} {:?} ", + gre_pdu.computed_ihl(), + gre_pdu.version(), + gre_pdu.ethertype(), + gre_pdu.has_checksum(), + gre_pdu.has_key(), + gre_pdu.has_sequence_number(), + gre_pdu.checksum(), + gre_pdu.computed_checksum(), + gre_pdu.key(), + gre_pdu.sequence_number() + ); +} + +#[allow(dead_code)] +fn print_icmp(icmp_pdu: IcmpPdu) { + print!( + "ICMP {:3} {:3} {:5} {:10}", + icmp_pdu.message_type(), + icmp_pdu.message_code(), + icmp_pdu.checksum(), + //icmp_pdu.computed_checksum(ip: &crate::Ip) + icmp_pdu.computed_data_offset() + ); +} + +#[allow(dead_code)] +fn print_ipv4(ipv4_pdu: Ipv4Pdu) { + let sa = ipv4_pdu.source_address(); + let da = ipv4_pdu.destination_address(); + print!( + "IPv4 {} {} {} {:2} {} {:4} {:5} {:5} {:5} {} {} {:3} {:2} {:5} {:5} {:03}:{:03}:{:03}:{:03} {:03}:{:03}:{:03}:{:03} ", + ipv4_pdu.version(), + ipv4_pdu.ihl(), + ipv4_pdu.computed_ihl(), + ipv4_pdu.dscp(), + ipv4_pdu.ecn(), + ipv4_pdu.total_length(), + ipv4_pdu.identification(), + ipv4_pdu.dont_fragment(), + ipv4_pdu.more_fragments(), + ipv4_pdu.fragment_offset(), + ipv4_pdu.computed_fragment_offset(), + ipv4_pdu.ttl(), + ipv4_pdu.protocol(), + ipv4_pdu.checksum(), + ipv4_pdu.computed_checksum(), + //ipv4_pdu.source_address(), + sa[0], + sa[1], + sa[2], + sa[3], + //ipv4_pdu.destination_address(), + da[0], + da[1], + da[2], + da[3] + //ipv4_pdu.options() + ); +} + +#[allow(dead_code)] +fn print_ipv6(ipv6_pdu: Ipv6Pdu) { + print!( + "IPv {:3} {:3} {:3} {:10} {:5} {:3} {:10} {:3} {:?} {:?} {:?} {:3} {:?} {:?}", + // the {:10} could maybe be shorter + // {:?} should be replaced with "proper" hexdecoding, aka a lib to do the displaying + ipv6_pdu.version(), + ipv6_pdu.dscp(), + ipv6_pdu.ecn(), + ipv6_pdu.flow_label(), + ipv6_pdu.payload_length(), + ipv6_pdu.next_header(), + ipv6_pdu.computed_ihl(), + ipv6_pdu.computed_protocol(), + ipv6_pdu.computed_identification(), + ipv6_pdu.computed_more_fragments(), + ipv6_pdu.computed_fragment_offset(), + ipv6_pdu.hop_limit(), + ipv6_pdu.source_address(), + ipv6_pdu.destination_address(), + //ipv6_pdu.extension_headers() + ); +} + +#[allow(dead_code)] +fn print_tcp(tcp_pdu: TcpPdu) { + print!( + "TCP {:5} {:5} {:10} {:10} {:2} {:2} {:2} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {:5} {} {} ", + tcp_pdu.source_port(), + tcp_pdu.destination_port(), + tcp_pdu.sequence_number(), + tcp_pdu.acknowledgement_number(), + tcp_pdu.data_offset(), + tcp_pdu.computed_data_offset(), + tcp_pdu.flags(), + tcp_pdu.fin(), + tcp_pdu.syn(), + tcp_pdu.rst(), + tcp_pdu.psh(), + tcp_pdu.ack(), + tcp_pdu.urg(), + tcp_pdu.ecn(), + tcp_pdu.cwr(), + tcp_pdu.window_size(), + //tcp_pdu.computed_window_size(shift: u8), + tcp_pdu.checksum(), + //tcp_pdu.computed_checksum(ip: &crate::Ip), + tcp_pdu.urgent_pointer(), + //tcp_pdu.options(), + ); +} + +#[allow(dead_code)] +fn print_udp(udp_pdu: UdpPdu) { + print!( + "UDP {:5} {:5} {:5} {:5} {} ", + udp_pdu.source_port(), + udp_pdu.destination_port(), + udp_pdu.length(), + udp_pdu.checksum(), + //udp_pdu.computed_checksum(ip: &crate::Ip), + udp_pdu.computed_data_offset() + ); +} + +#[allow(unused_variables)] +fn filter_ipv4_tcp_raw(ipv4_pdu: Ipv4Pdu, tcp_pdu: TcpPdu, raw: &[u8]) -> Verdict { + // example filter that drops GET requests to /secret + lazy_static! { + static ref RE: Regex = Regex::new("^GET /secret ").expect("Could not compile Regex!"); + } + if RE.is_match(raw) { + Verdict::Drop + } else { + Verdict::Accept + } +}