diff --git a/nflux-ebpf/src/main.rs b/nflux-ebpf/src/main.rs index 83c86fa..3e8b4ed 100644 --- a/nflux-ebpf/src/main.rs +++ b/nflux-ebpf/src/main.rs @@ -11,7 +11,7 @@ use aya_ebpf::{ programs::XdpContext, }; use core::mem; -use network_types::ip::IpProto; +use network_types::ip::{IpProto, Ipv6Hdr}; use network_types::{ eth::{EthHdr, EtherType}, ip::Ipv4Hdr, @@ -97,7 +97,6 @@ fn start_nflux(ctx: XdpContext) -> Result { let ack = unsafe { (*tcphdr).ack() }; if syn == 1 && ack == 0 { - // SYN packet: Apply rules for incoming or outgoing connections if rule.ports.contains(&dst_port) { if rule.action == 1 { log_new_connection(ctx, source_ip, dst_port, 6); @@ -108,14 +107,11 @@ fn start_nflux(ctx: XdpContext) -> Result { } } } else if syn == 1 && ack == 1 { - // SYN-ACK packets: Allow for outgoing connection responses return Ok(xdp_action::XDP_PASS); } else if ack == 1 { - // ACK packets: Allow for established connections return Ok(xdp_action::XDP_PASS); } - // For other TCP packets, apply rules if rule.ports.contains(&dst_port) { if rule.action == 1 { log_new_connection(ctx, source_ip, dst_port, 6); @@ -134,27 +130,94 @@ fn start_nflux(ctx: XdpContext) -> Result { log_new_connection(ctx, source_ip, dst_port, 17); return Ok(xdp_action::XDP_PASS); } - // By the moment, allow every UDP packet - // Necessary to allow DNS UDP packets (internet browsing, for example) return Ok(xdp_action::XDP_PASS); } IpProto::Icmp => { - // Read from EBPF map if let Some(&icmp_ping) = ICMP_RULE.get(0) { if icmp_ping == 1 { - // Allow ICMP packets if enabled log_new_connection(ctx, source_ip, 0, 1); return Ok(xdp_action::XDP_PASS); } } - // Block ICMP packets by default return Ok(xdp_action::XDP_DROP); } _ => return Ok(xdp_action::XDP_DROP), } } } + Ok(xdp_action::XDP_DROP) + } + EtherType::Ipv6 => { + let ipv6hdr: *const Ipv6Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? }; + let proto = unsafe { (*ipv6hdr).next_hdr }; + let source_ip = unsafe { (*ipv6hdr).src_addr.in6_u.u6_addr8 }; + + for prefix_len in (1..=128).rev() { + let key = Key::new( + prefix_len, + LpmKeyIpv6 { + prefix_len, + ip: source_ip, + }, + ); + + if let Some(rule) = IPV6_RULES.get(&key) { + match proto { + IpProto::Tcp => { + let tcphdr: *const TcpHdr = + unsafe { ptr_at(&ctx, EthHdr::LEN + Ipv6Hdr::LEN)? }; + let dst_port = u16::from_be(unsafe { (*tcphdr).dest }); + let syn = unsafe { (*tcphdr).syn() }; + let ack = unsafe { (*tcphdr).ack() }; + + if syn == 1 && ack == 0 { + if rule.ports.contains(&dst_port) { + if rule.action == 1 { + log_new_connection(ctx, 0, dst_port, 6); + return Ok(xdp_action::XDP_PASS); + } else { + log_new_connection(ctx, 0, dst_port, 6); + return Ok(xdp_action::XDP_DROP); + } + } + } else if syn == 1 && ack == 1 { + return Ok(xdp_action::XDP_PASS); + } else if ack == 1 { + return Ok(xdp_action::XDP_PASS); + } + if rule.ports.contains(&dst_port) { + if rule.action == 1 { + log_new_connection(ctx, 0, 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 + Ipv6Hdr::LEN)? }; + let dst_port = u16::from_be(unsafe { (*udphdr).dest }); + + if rule.ports.contains(&dst_port) && rule.action == 1 { + log_new_connection(ctx, 0, dst_port, 17); + return Ok(xdp_action::XDP_PASS); + } + return Ok(xdp_action::XDP_PASS); + } + IpProto::Icmp => { + if let Some(&icmp_ping) = ICMP_RULE.get(0) { + if icmp_ping == 1 { + log_new_connection(ctx, 0, 0, 1); + return Ok(xdp_action::XDP_PASS); + } + } + return Ok(xdp_action::XDP_DROP); + } + _ => return Ok(xdp_action::XDP_DROP), + } + } + } Ok(xdp_action::XDP_DROP) } _ => Ok(xdp_action::XDP_DROP), diff --git a/nflux.toml b/nflux.toml index 850e3bb..f69f9d5 100644 --- a/nflux.toml +++ b/nflux.toml @@ -12,13 +12,14 @@ log_type = "text" # text or json. Defaults to text if not set [ip_rules] # The /32 CIDR block is used to represent a single IP address rather than a range -"192.168.0.174/32" = { priority = 1, action = "allow", ports = [22], protocol = "tcp", log = false, description = "Allow SSH for specific IP" } -"192.168.0.0/24" = { priority = 2, action = "deny", ports = [8081], protocol = "tcp", log = false, description = "Deny SSH for entire subnet" } +"192.168.0.0/24" = { priority = 1, action = "allow", ports = [22], protocol = "tcp", log = false, description = "Allow SSH for entire local net" } -# todo: ipv6 support -# "2001:0db8:85a3:0000:0000:8a2e:0370:7334" = { action = "deny", ports = [80], protocol = "tcp" } +# curl -6 -v http://\[::ffff:192.168.0.26\]:80 +"fe80::5bc2:662b:ac2f:7e8b/128" = { priority = 2, action = "allow", ports = [80], protocol = "tcp", log = false, description = "Deny HTTP for specific IPv6 address" } -# [mac_rules] -# # Rules for MAC address filtering -# "00:0a:95:9d:68:16" = { action = "allow" } -# "00:0a:95:9d:68:17" = { action = "deny" } +#[mac_rules] +# todo: MAC address filtering +# This is not implemented. Not necessary by the moment +# Rules for MAC address filtering +#"00:0a:95:9d:68:16" = { action = "allow" } +#"00:0a:95:9d:68:17" = { action = "deny" } diff --git a/nflux/src/main.rs b/nflux/src/main.rs index d78fc92..02b5314 100644 --- a/nflux/src/main.rs +++ b/nflux/src/main.rs @@ -15,9 +15,9 @@ use bytes::BytesMut; use config::{Action, Nflux, Protocol, IpRules}; use core::set_mem_limit; use logger::setup_logger; -use nflux_common::{convert_protocol, ConnectionEvent, IpRule, LpmKeyIpv4}; +use nflux_common::{convert_protocol, ConnectionEvent, IpRule, LpmKeyIpv4, LpmKeyIpv6}; use std::collections::HashMap; -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::ptr; use tokio::task; use tracing::{error, info}; @@ -45,9 +45,8 @@ async fn main() -> anyhow::Result<()> { let mut bpf = Ebpf::load(include_bytes_aligned!(concat!(env!("OUT_DIR"), "/nflux")))?; // Populate eBPF maps with configuration data - populate_ipv4_rules(&mut bpf, &config.ip_rules)?; + populate_ip_rules(&mut bpf, &config.ip_rules)?; populate_icmp_rule(&mut bpf, config.nflux.icmp_ping)?; - // populate_ipv6_rules(&mut bpf, &config.ip_rules)?; // Attach XDP program let program: &mut Xdp = bpf.program_mut("nflux").unwrap().try_into()?; @@ -123,36 +122,6 @@ fn parse_connection_event(buf: &BytesMut) -> anyhow::Result { } } -fn populate_ipv4_rules(bpf: &mut Ebpf, ip_rules: &HashMap) -> anyhow::Result<()> { - let mut ipv4_map: LpmTrie<&mut MapData, LpmKeyIpv4, IpRule> = LpmTrie::try_from( - bpf.map_mut("IPV4_RULES") - .context("Failed to find IPV4_RULES map")?, - )?; - - // Sort rules by priority - let mut sorted_rules: Vec<_> = ip_rules.iter().collect(); - sorted_rules.sort_by_key(|(_, rule)| rule.priority); - - for (cidr, rule) in sorted_rules { - let (ip, prefix_len) = parse_cidr_v4(cidr)?; - let ip_rule = prepare_ip_rule(rule)?; - - let key = Key::new( - prefix_len, - LpmKeyIpv4 { - prefix_len, - ip: ip.into(), - }, - ); - - ipv4_map - .insert(&key, &ip_rule, 0) - .context("Failed to insert IPv4 rule")?; - } - - Ok(()) -} - fn prepare_ip_rule(rule: &IpRules) -> anyhow::Result { let mut ports = [0u16; 16]; for (i, &port) in rule.ports.iter().enumerate().take(16) { @@ -173,22 +142,6 @@ fn prepare_ip_rule(rule: &IpRules) -> anyhow::Result { }) } -// fn populate_ipv6_rules(bpf: &mut Ebpf, ip_rules: &HashMap) -> anyhow::Result<()> { -// let mut ipv6_map: LpmTrie<&mut MapData, LpmKeyIpv6, IpRule> = LpmTrie::try_from( -// bpf.map_mut("IPV6_RULES").context("Failed to find IPV4_RULES map")?, -// )?; - -// for (cidr, rule) in ip_rules { -// let (ip, prefix_len) = parse_cidr_v6(cidr)?; -// let ip_rule = prepare_ip_rule(rule)?; - -// let key = Key::new(prefix_len, LpmKeyIpv6 { prefix_len, ip: ip.into() }); -// ipv6_map.insert(&key, &ip_rule, 0).context("Failed to insert IPv6 rule")?; -// } - -// Ok(()) -// } - fn parse_cidr_v4(cidr: &str) -> anyhow::Result<(Ipv4Addr, u32)> { let parts: Vec<&str> = cidr.split('/').collect(); if parts.len() != 2 { @@ -199,12 +152,74 @@ fn parse_cidr_v4(cidr: &str) -> anyhow::Result<(Ipv4Addr, u32)> { Ok((ip, prefix_len)) } -// fn parse_cidr_v6(cidr: &str) -> anyhow::Result<(Ipv6Addr, u32)> { -// let parts: Vec<&str> = cidr.split('/').collect(); -// if parts.len() != 2 { -// return Err(anyhow::anyhow!("Invalid CIDR format: {}", cidr)); -// } -// let ip = parts[0].parse::()?; -// let prefix_len = parts[1].parse::()?; -// Ok((ip, prefix_len)) -// } +fn parse_cidr_v6(cidr: &str) -> anyhow::Result<(Ipv6Addr, u32)> { + let parts: Vec<&str> = cidr.split('/').collect(); + if parts.len() != 2 { + return Err(anyhow::anyhow!("Invalid CIDR format: {}", cidr)); + } + let ip = parts[0].parse::()?; + let prefix_len = parts[1].parse::()?; + Ok((ip, prefix_len)) +} + +fn populate_ip_rules(bpf: &mut Ebpf, ip_rules: &HashMap) -> anyhow::Result<()> { + { + // Populate IPv4 rules + let mut ipv4_map: LpmTrie<&mut MapData, LpmKeyIpv4, IpRule> = LpmTrie::try_from( + bpf.map_mut("IPV4_RULES") + .context("Failed to find IPV4_RULES map")?, + )?; + + // Sort rules by priority + let mut sorted_rules: Vec<_> = ip_rules.iter().collect(); + sorted_rules.sort_by_key(|(_, rule)| rule.priority); + + for (cidr, rule) in &sorted_rules { + if let Ok((ip, prefix_len)) = parse_cidr_v4(cidr) { + // Handle IPv4 rules + let ip_rule = prepare_ip_rule(rule)?; + let key = Key::new( + prefix_len, + LpmKeyIpv4 { + prefix_len, + ip: ip.into(), + }, + ); + ipv4_map + .insert(&key, &ip_rule, 0) + .context("Failed to insert IPv4 rule")?; + } + } + } + + { + // Populate IPv6 rules + let mut ipv6_map: LpmTrie<&mut MapData, LpmKeyIpv6, IpRule> = LpmTrie::try_from( + bpf.map_mut("IPV6_RULES") + .context("Failed to find IPV6_RULES map")?, + )?; + + // Sort rules by priority + let mut sorted_rules: Vec<_> = ip_rules.iter().collect(); + sorted_rules.sort_by_key(|(_, rule)| rule.priority); + + for (cidr, rule) in &sorted_rules { + if let Ok((ip, prefix_len)) = parse_cidr_v6(cidr) { + // Handle IPv6 rules + let ip_rule = prepare_ip_rule(rule)?; + let key = Key::new( + prefix_len, + LpmKeyIpv6 { + prefix_len, + ip: ip.octets(), + }, + ); + ipv6_map + .insert(&key, &ip_rule, 0) + .context("Failed to insert IPv6 rule")?; + } + } + } + + Ok(()) +}