From 784f360a52abe99c6cb0fd2fad3bd6da270d6c4a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 03:14:28 +0000 Subject: [PATCH] Add IPv6 support to TCP flag anomaly detection - TcpAnomalyEvent: replace src_ip/dst_ip u32 with [u64; 2] to encode both IPv4 and IPv6 without u128 alignment issues; rename _pad to is_ipv6 - ingress/egress anomaly.rs: add ipv6_check_and_emit using hi/lo u64 split - ingress/egress main.rs: call ipv6_check_and_emit in IPv6 XDP arm and drop on hit - alert.rs: update from_tcp_anomaly to branch on is_ipv6 and reconstruct u128 from [hi, lo] before formatting; import format_ipv6 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0138PxtKH73hqxv7h1oaoSdS --- common/src/model/tcp_anomaly.rs | 13 +++-- egress-ebpf/src/action/anomaly.rs | 78 ++++++++++++++++++++---------- egress-ebpf/src/main.rs | 3 ++ ingress-ebpf/src/action/anomaly.rs | 78 ++++++++++++++++++++---------- ingress-ebpf/src/main.rs | 3 ++ mantis/src/model/alert.rs | 14 ++++-- 6 files changed, 131 insertions(+), 58 deletions(-) diff --git a/common/src/model/tcp_anomaly.rs b/common/src/model/tcp_anomaly.rs index aa94aa1..0608efd 100644 --- a/common/src/model/tcp_anomaly.rs +++ b/common/src/model/tcp_anomaly.rs @@ -8,18 +8,21 @@ pub const DIRECTION_EGRESS: u8 = 1; /// TCP flag anomaly event written to the ring buffer by XDP programs. /// -/// align_of::() == 8, satisfying the aya ring buffer constraint -/// that 8 % align_of::() == 0. +/// src_ip/dst_ip are stored as [hi_u64, lo_u64] (big-endian halves of the address). +/// For IPv4: src_ip = [v4_addr as u64, 0], is_ipv6 = 0. +/// For IPv6: src_ip = [addr >> 64, addr as u64], is_ipv6 = 1. +/// +/// align_of == 8, satisfying the aya ring buffer constraint (8 % 8 == 0). #[repr(C)] #[derive(Clone, Copy)] pub struct TcpAnomalyEvent { pub timestamp_ns: u64, - pub src_ip: u32, - pub dst_ip: u32, + pub src_ip: [u64; 2], + pub dst_ip: [u64; 2], pub src_port: u16, pub dst_port: u16, pub flags: u8, pub anomaly_type: u8, pub direction: u8, - pub _pad: u8, + pub is_ipv6: u8, } diff --git a/egress-ebpf/src/action/anomaly.rs b/egress-ebpf/src/action/anomaly.rs index 7745568..65ca894 100644 --- a/egress-ebpf/src/action/anomaly.rs +++ b/egress-ebpf/src/action/anomaly.rs @@ -1,6 +1,6 @@ use aya_ebpf::macros::map; use aya_ebpf::maps::RingBuf; -use common::model::event::IPv4Event; +use common::model::event::{IPv4Event, IPv6Event}; use common::model::tcp_anomaly::{ ANOMALY_NULL_SCAN, ANOMALY_RST_SYN, ANOMALY_SYN_FIN, ANOMALY_XMAS_SCAN, DIRECTION_EGRESS, TcpAnomalyEvent, @@ -10,41 +10,69 @@ use network_types::ip::IpProto; #[map] pub static TCP_ANOMALY_EVENTS: RingBuf = RingBuf::with_byte_size(1 << 18, 0); -/// Returns true if a TCP flag anomaly was detected (and emitted to the ring buffer). +#[inline(always)] +fn anomaly_type(flags_byte: u8) -> Option { + if flags_byte == 0 { + Some(ANOMALY_NULL_SCAN) + } else if flags_byte & 0x03 == 0x03 { + Some(ANOMALY_SYN_FIN) + } else if flags_byte & 0x29 == 0x29 { + Some(ANOMALY_XMAS_SCAN) + } else if flags_byte & 0x06 == 0x06 { + Some(ANOMALY_RST_SYN) + } else { + None + } +} + +#[inline(always)] +fn emit(ev: TcpAnomalyEvent) { + if let Some(mut entry) = TCP_ANOMALY_EVENTS.reserve::(0) { + entry.write(ev); + entry.submit(0); + } +} + +/// Returns true if a TCP flag anomaly was detected and emitted to the ring buffer. #[inline(always)] pub fn ipv4_check_and_emit(event: &IPv4Event) -> bool { if event.protocol != IpProto::Tcp { return false; } let flags_byte = event.tcp_flags.to_byte(); - let anomaly_type = if flags_byte == 0 { - ANOMALY_NULL_SCAN - } else if flags_byte & 0x03 == 0x03 { - ANOMALY_SYN_FIN - } else if flags_byte & 0x29 == 0x29 { - ANOMALY_XMAS_SCAN - } else if flags_byte & 0x06 == 0x06 { - ANOMALY_RST_SYN - } else { - return false; - }; - - let ev = TcpAnomalyEvent { + let Some(anomaly_type) = anomaly_type(flags_byte) else { return false }; + emit(TcpAnomalyEvent { timestamp_ns: event.timestamp_us.wrapping_mul(1000), - src_ip: event.src_ip, - dst_ip: event.dst_ip, + src_ip: [event.src_ip as u64, 0], + dst_ip: [event.dst_ip as u64, 0], src_port: event.src_port, dst_port: event.dst_port, flags: flags_byte, anomaly_type, direction: DIRECTION_EGRESS, - _pad: 0, - }; - - if let Some(mut entry) = TCP_ANOMALY_EVENTS.reserve::(0) { - entry.write(ev); - entry.submit(0); - } - + is_ipv6: 0, + }); + true +} + +/// Returns true if a TCP flag anomaly was detected and emitted to the ring buffer. +#[inline(always)] +pub fn ipv6_check_and_emit(event: &IPv6Event) -> bool { + if event.protocol != IpProto::Tcp { + return false; + } + let flags_byte = event.tcp_flags.to_byte(); + let Some(anomaly_type) = anomaly_type(flags_byte) else { return false }; + emit(TcpAnomalyEvent { + timestamp_ns: event.timestamp_us.wrapping_mul(1000), + src_ip: [(event.src_ip >> 64) as u64, event.src_ip as u64], + dst_ip: [(event.dst_ip >> 64) as u64, event.dst_ip as u64], + src_port: event.src_port, + dst_port: event.dst_port, + flags: flags_byte, + anomaly_type, + direction: DIRECTION_EGRESS, + is_ipv6: 1, + }); true } diff --git a/egress-ebpf/src/main.rs b/egress-ebpf/src/main.rs index f0bb159..e51d142 100644 --- a/egress-ebpf/src/main.rs +++ b/egress-ebpf/src/main.rs @@ -80,6 +80,9 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result { if access_control::ipv6_is_blacklisted(event) { return Ok(xdp_action::XDP_DROP); } + if anomaly::ipv6_check_and_emit(event) { + return Ok(xdp_action::XDP_DROP); + } } } let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS); diff --git a/ingress-ebpf/src/action/anomaly.rs b/ingress-ebpf/src/action/anomaly.rs index 01d6631..3fad478 100644 --- a/ingress-ebpf/src/action/anomaly.rs +++ b/ingress-ebpf/src/action/anomaly.rs @@ -1,6 +1,6 @@ use aya_ebpf::macros::map; use aya_ebpf::maps::RingBuf; -use common::model::event::IPv4Event; +use common::model::event::{IPv4Event, IPv6Event}; use common::model::tcp_anomaly::{ ANOMALY_NULL_SCAN, ANOMALY_RST_SYN, ANOMALY_SYN_FIN, ANOMALY_XMAS_SCAN, DIRECTION_INGRESS, TcpAnomalyEvent, @@ -10,41 +10,69 @@ use network_types::ip::IpProto; #[map] pub static TCP_ANOMALY_EVENTS: RingBuf = RingBuf::with_byte_size(1 << 18, 0); -/// Returns true if a TCP flag anomaly was detected (and emitted to the ring buffer). +#[inline(always)] +fn anomaly_type(flags_byte: u8) -> Option { + if flags_byte == 0 { + Some(ANOMALY_NULL_SCAN) + } else if flags_byte & 0x03 == 0x03 { + Some(ANOMALY_SYN_FIN) + } else if flags_byte & 0x29 == 0x29 { + Some(ANOMALY_XMAS_SCAN) + } else if flags_byte & 0x06 == 0x06 { + Some(ANOMALY_RST_SYN) + } else { + None + } +} + +#[inline(always)] +fn emit(ev: TcpAnomalyEvent) { + if let Some(mut entry) = TCP_ANOMALY_EVENTS.reserve::(0) { + entry.write(ev); + entry.submit(0); + } +} + +/// Returns true if a TCP flag anomaly was detected and emitted to the ring buffer. #[inline(always)] pub fn ipv4_check_and_emit(event: &IPv4Event) -> bool { if event.protocol != IpProto::Tcp { return false; } let flags_byte = event.tcp_flags.to_byte(); - let anomaly_type = if flags_byte == 0 { - ANOMALY_NULL_SCAN - } else if flags_byte & 0x03 == 0x03 { - ANOMALY_SYN_FIN - } else if flags_byte & 0x29 == 0x29 { - ANOMALY_XMAS_SCAN - } else if flags_byte & 0x06 == 0x06 { - ANOMALY_RST_SYN - } else { - return false; - }; - - let ev = TcpAnomalyEvent { + let Some(anomaly_type) = anomaly_type(flags_byte) else { return false }; + emit(TcpAnomalyEvent { timestamp_ns: event.timestamp_us.wrapping_mul(1000), - src_ip: event.src_ip, - dst_ip: event.dst_ip, + src_ip: [event.src_ip as u64, 0], + dst_ip: [event.dst_ip as u64, 0], src_port: event.src_port, dst_port: event.dst_port, flags: flags_byte, anomaly_type, direction: DIRECTION_INGRESS, - _pad: 0, - }; - - if let Some(mut entry) = TCP_ANOMALY_EVENTS.reserve::(0) { - entry.write(ev); - entry.submit(0); - } - + is_ipv6: 0, + }); + true +} + +/// Returns true if a TCP flag anomaly was detected and emitted to the ring buffer. +#[inline(always)] +pub fn ipv6_check_and_emit(event: &IPv6Event) -> bool { + if event.protocol != IpProto::Tcp { + return false; + } + let flags_byte = event.tcp_flags.to_byte(); + let Some(anomaly_type) = anomaly_type(flags_byte) else { return false }; + emit(TcpAnomalyEvent { + timestamp_ns: event.timestamp_us.wrapping_mul(1000), + src_ip: [(event.src_ip >> 64) as u64, event.src_ip as u64], + dst_ip: [(event.dst_ip >> 64) as u64, event.dst_ip as u64], + src_port: event.src_port, + dst_port: event.dst_port, + flags: flags_byte, + anomaly_type, + direction: DIRECTION_INGRESS, + is_ipv6: 1, + }); true } diff --git a/ingress-ebpf/src/main.rs b/ingress-ebpf/src/main.rs index 75a6187..24d3cec 100644 --- a/ingress-ebpf/src/main.rs +++ b/ingress-ebpf/src/main.rs @@ -82,6 +82,9 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result { if access_control::ipv6_is_blacklisted(event) { return Ok(xdp_action::XDP_DROP); } + if anomaly::ipv6_check_and_emit(event) { + return Ok(xdp_action::XDP_DROP); + } } } let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS); diff --git a/mantis/src/model/alert.rs b/mantis/src/model/alert.rs index d61cefe..ac48d6b 100644 --- a/mantis/src/model/alert.rs +++ b/mantis/src/model/alert.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::model::ml_detection::DetectionResult; use crate::model::rule_detection::RuleMatch; -use crate::utils::packet_parser::format_ipv4; +use crate::utils::packet_parser::{format_ipv4, format_ipv6}; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "snake_case")] @@ -127,8 +127,16 @@ impl UnifiedAlert { } pub fn from_tcp_anomaly(event: &TcpAnomalyEvent) -> Self { - let src_ip = format_ipv4(event.src_ip).to_string(); - let dst_ip = format_ipv4(event.dst_ip).to_string(); + let (src_ip, dst_ip) = if event.is_ipv6 == 0 { + ( + format_ipv4(event.src_ip[0] as u32).to_string(), + format_ipv4(event.dst_ip[0] as u32).to_string(), + ) + } else { + let src_v6 = (event.src_ip[0] as u128) << 64 | event.src_ip[1] as u128; + let dst_v6 = (event.dst_ip[0] as u128) << 64 | event.dst_ip[1] as u128; + (format_ipv6(src_v6).to_string(), format_ipv6(dst_v6).to_string()) + }; let flow_key = format!("{}:{}-{}:{}", src_ip, event.src_port, dst_ip, event.dst_port); let direction = if event.direction == DIRECTION_INGRESS { "ingress" } else { "egress" }; let attack_type = match event.anomaly_type {