Skip to content

Commit

Permalink
Working with rule implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
containerscrew committed Dec 3, 2024
1 parent cb36391 commit 89e66bc
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 369 deletions.
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 20 additions & 18 deletions nflux-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,36 @@ pub struct ConnectionEvent {
pub protocol: u8, // 6 for TCP, 17 for UDP
}

// #[repr(C)]
// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
// pub struct GlobalFirewallRules {
// pub icmp_enabled: u8,
// pub allowed_ipv4: [u32; MAX_ALLOWED_IPV4],
// pub allowed_ports: [u32; MAX_ALLOWED_PORTS],
// }

// #[cfg(feature = "user")]
// pub mod user {
// use super::*;

// unsafe impl aya::Pod for GlobalFirewallRules {}
// }

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct Ipv4Rule {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IpRule {
pub action: u8, // 0 = deny, 1 = allow
pub ports: [u16; 16], // Up to 16 ports
pub protocol: u8, // 6 = TCP, 17 = UDP
pub priority: u32, // Lower number means higher priority
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct LpmKeyIpv4 {
pub prefix_len: u32,
pub ip: u32,
}

#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct LpmKeyIpv6 {
pub prefix_len: u32,
pub ip: [u8; 16],
}

#[cfg(feature = "user")]
pub mod user {
use super::*;

unsafe impl aya::Pod for Ipv4Rule {}
unsafe impl aya::Pod for IpRule {}
unsafe impl aya::Pod for LpmKeyIpv4 {}
unsafe impl aya::Pod for LpmKeyIpv6 {}
}

// Define the default configuration if the user does not provide one
Expand Down
207 changes: 56 additions & 151 deletions nflux-ebpf/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,34 @@
#![no_main]
#![allow(nonstandard_style, dead_code)]

use aya_ebpf::helpers::bpf_ktime_get_ns;
use aya_ebpf::maps::lpm_trie::Key;
use aya_ebpf::maps::LpmTrie;
use aya_ebpf::{
bindings::xdp_action,
macros::{map, xdp},
maps::{LruHashMap, PerfEventArray},
maps::PerfEventArray,
programs::XdpContext,
};

use core::mem;
use network_types::{
eth::{EthHdr, EtherType},
ip::{IpProto, Ipv4Hdr},
tcp::TcpHdr,
udp::UdpHdr,
};

use nflux_common::{ConnectionEvent, Ipv4Rule};
use nflux_common::{ConnectionEvent, IpRule, LpmKeyIpv4, LpmKeyIpv6};

#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}

// #[map]
// static GLOBAL_FIREWALL_RULES: Array<GlobalFirewallRules> = Array::with_max_entries(1, 0);
#[map]
static IPV4_RULES: LpmTrie<LpmKeyIpv4, IpRule> = LpmTrie::with_max_entries(1024, 0);

#[map]
static IPV4_RULES: LruHashMap<u32, Ipv4Rule> = LruHashMap::with_max_entries(1024, 0);
static IPV6_RULES: LpmTrie<LpmKeyIpv6, IpRule> = LpmTrie::with_max_entries(1024, 0);

#[map]
static CONNECTION_EVENTS: PerfEventArray<ConnectionEvent> = PerfEventArray::new(0);
Expand All @@ -56,167 +55,73 @@ unsafe fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Result<*const T, ()> {
Ok((start + offset) as *const T)
}

// Check if a port is allowed
fn is_port_allowed(global_firewall_rules: &GlobalFirewallRules, port: u16) -> bool {
for &allowed_port in &global_firewall_rules.allowed_ports {
if allowed_port == 0 {
// Stop if we hit an uninitialized entry (assuming 0 indicates unused entries)
break;
}
if port as u32 == allowed_port {
return true;
}
}
false
}

// Check if an IP address is allowed
fn is_ipv4_allowed(app_config: &GlobalFirewallRules, ip: u32) -> bool {
for &allowed_ip in &app_config.allowed_ipv4 {
if allowed_ip == 0 {
// Stop if we hit an uninitialized entry (assuming 0 indicates unused entries)
break;
}
if ip == allowed_ip {
return true;
}
}
false
}

// Helper function to get the current time in nanoseconds
fn current_time_ns() -> u64 {
unsafe { bpf_ktime_get_ns() }
}

#[repr(C)]
struct IpPort {
ip: u32,
port: u16,
}

#[map]
static RECENT_LOGS: LruHashMap<IpPort, u64> = LruHashMap::with_max_entries(1024, 0);

// Function to check if we should log a dropped SYN packet to avoid excessive logging
fn should_log(ip: u32, port: u16, log_interval_secs: u64) -> bool {
let key = IpPort { ip, port };
let now = current_time_ns();

unsafe {
if let Some(&last_logged) = RECENT_LOGS.get(&key) {
// Only log if more than 5 seconds have passed
if now - last_logged < log_interval_secs * 1_000_000_000 {
return false;
}
}
}

// Update the map with the new timestamp
RECENT_LOGS.insert(&key, &now, 0).ok();
true
}

fn get_global_firewall_rules() -> &'static GlobalFirewallRules {
GLOBAL_FIREWALL_RULES.get(0).unwrap()
}

fn log_new_connection(
ctx: XdpContext,
src_addr: u32,
dst_port: u16,
protocol: u8,
log_interval_secs: u64,
) {
fn log_new_connection(ctx: XdpContext, src_addr: u32, dst_port: u16, protocol: u8) {
let event = ConnectionEvent {
src_addr,
dst_port,
protocol,
};

if should_log(src_addr, dst_port, log_interval_secs) {
CONNECTION_EVENTS.output(&ctx, &event, 0);
}
CONNECTION_EVENTS.output(&ctx, &event, 0);
}

fn start_nflux(ctx: XdpContext) -> Result<u32, ()> {
let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? };

// Get global firewall rules
let global_firewall_rules = get_global_firewall_rules();

match unsafe { (*ethhdr).ether_type } {
EtherType::Ipv4 => {
let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? };
let source = u32::from_be(unsafe { (*ipv4hdr).src_addr });
let source_ip = u32::from_be(unsafe { (*ipv4hdr).src_addr });
let proto = unsafe { (*ipv4hdr).proto };

match proto {
IpProto::Tcp => {
// Parse the TCP header
let tcphdr: *const TcpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)? };
let dst_port = u16::from_be(unsafe { (*tcphdr).dest });

// if is_port_allowed(&global_firewall_rules, dst_port) {
// log_new_connection(ctx, source, dst_port, 6, 5);
// return Ok(xdp_action::XDP_PASS);
// }

// // Check if the IP address is allowed
// if is_ipv4_allowed(&global_firewall_rules, source) {
// log_new_connection(ctx, source, dst_port, 6, 5);
// return Ok(xdp_action::XDP_PASS);
// }

// // Deny incoming connections, except SYN-ACK packets
// if unsafe { (*tcphdr).syn() == 1 && (*tcphdr).ack() == 0 } {
// // Block unsolicited incoming SYN packets (deny incoming connections)
// return Ok(xdp_action::XDP_DROP);
// } else if unsafe { (*tcphdr).ack() == 1 } {
// // Permit ACK packets (responses to outgoing connections)
// log_new_connection(ctx, source, dst_port, 6, 5);
// return Ok(xdp_action::XDP_PASS);
// }

Ok(xdp_action::XDP_DROP)
}
IpProto::Udp => {
// Parse UDP header
let udphdr: *const UdpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)? };
let dst_port = u16::from_be(unsafe { (*udphdr).dest });
let src_port = u16::from_be(unsafe { (*udphdr).source });

// If the source port (DNS) is 53, allow the packet. Internet connection
// if src_port == 53 {
// return Ok(xdp_action::XDP_PASS);
// }

// // Check if the IP address is blocked
// if is_ipv4_allowed(&global_firewall_rules, source) {
// log_new_connection(ctx, source, dst_port, 6, 5);
// return Ok(xdp_action::XDP_PASS);
// }

// // Check allowed ports
// if is_port_allowed(&global_firewall_rules, dst_port) {
// log_new_connection(ctx, source, dst_port, 6, 5);
// return Ok(xdp_action::XDP_PASS);
// }

Ok(xdp_action::XDP_DROP)
let key = Key::new(
32,
LpmKeyIpv4 {
prefix_len: 32,
ip: source_ip,
},
);

if let Some(rule) = IPV4_RULES.get(&key) {
match proto {
IpProto::Tcp => {
let tcphdr: *const TcpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)? };
let dst_port = u16::from_be(unsafe { (*tcphdr).dest });

if rule.ports.contains(&dst_port) {
if rule.action == 1 {
log_new_connection(ctx, source_ip, dst_port, 6);
return Ok(xdp_action::XDP_PASS);
}
}
return Ok(xdp_action::XDP_DROP);
}
IpProto::Udp => {
let udphdr: *const UdpHdr =
unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)? };
let dst_port = u16::from_be(unsafe { (*udphdr).dest });

if rule.ports.contains(&dst_port) {
if rule.action == 1 {
log_new_connection(ctx, source_ip, dst_port, 17);
return Ok(xdp_action::XDP_PASS);
}
}
return Ok(xdp_action::XDP_DROP);
}
IpProto::Icmp => {
if rule.action == 1 {
log_new_connection(ctx, source_ip, 0, 1);
return Ok(xdp_action::XDP_PASS);
}
return Ok(xdp_action::XDP_DROP);
}
_ => return Ok(xdp_action::XDP_DROP),
}
// IpProto::Icmp => {
// if global_firewall_rules.allow_icmp == 1 {
// log_new_connection(ctx, source, 0, 1, 5);
// Ok(xdp_action::XDP_PASS)
// } else {
// Ok(xdp_action::XDP_DROP)
// }
// }
_ => Ok(xdp_action::XDP_DROP),
}

Ok(xdp_action::XDP_DROP)
}
_ => Ok(xdp_action::XDP_DROP),
}
Expand Down
50 changes: 31 additions & 19 deletions nflux.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
[firewall]
# Applies for ipv4 and ipv6
# Global means, if you put here an ip will be able to access every port, tcp and udp
# enabled = true/false #TODO: Implement this
icmp_enabled = true
interface_name = "wlp2s0"
log_level = "info" # trace, debug, info, warn or error. Defaults to info if not set
log_type = "text" # text or json. Defaults to text if not set
# TODO: Add support for multiple interfaces
interface_names = ["wlp2s0", "eth0"]
log_level = "info" # trace, debug, info, warn, or error. Defaults to info if not set
log_type = "text" # text or json. Defaults to text if not set
# TODO
# default_action = "deny" # global default action if no specific rule matches

[firewall.ipv4_rules]
# This is more finetuned, you can specify which ip can access which port
"192.168.0.4" = { action = "deny", ports = [80], protocol = "tcp" }
"192.168.0.11" = { action = "allow", ports = [53], protocol = "udp"}
"192.168.0.50" = { action = "deny", ports = [22, 443], protocol = "tcp" }
"192.168.0.100" = { action = "allow", ports = [80, 8080], protocol = "tcp"}
[ip_rules]
# Fine-tuned rules for IP-based filtering
"192.168.0.0/24" = { priority = 1, action = "deny", ports = [22], protocol = "tcp" }
"192.168.0.170/32" = { priority = 1, action = "allow", ports = [22], protocol = "tcp", log = true, description = "Block SSH for single IP" }
# "192.168.0.170/24" = { priority = 2, action = "deny", ports = [22], protocol = "tcp", log = false, description = "Deny SSH from entire subnet" }
# "2001:0db8:85a3:0000:0000:8a2e:0370:7334" = { action = "deny", ports = [80], protocol = "tcp" }

[firewall.ipv6_rules]
"2001:0db8:85a3:0000:0000:8a2e:0370:7334" = { action = "deny", ports = [80], protocol = "tcp" }
[icmp_rules]
# Rules for ICMP traffic
"192.168.0.1/24" = { action = "deny", protocol = "icmp" }
"192.168.0.88/24" = { action = "allow", protocol = "icmp" }
"192.168.0.22/24" = { action = "deny", protocol = "icmp" }

[firewall.icmp_rules]
"192.168.0.1" = { action = "deny" }
"192.168.0.88" = { action = "allow" }
"192.168.0.22" = { action = "deny" }
[mac_rules]
# Rules for MAC address filtering
"00:0a:95:9d:68:16" = { action = "allow" }
"00:0a:95:9d:68:17" = { action = "deny" }

[logging]
log_denied_packets = true
log_allowed_packets = false
log_format = "json"
log_file = "/var/log/firewall.log"

[failsafe]
# Failsafe rule for unmatched traffic
action = "log"
Loading

0 comments on commit 89e66bc

Please sign in to comment.