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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138PxtKH73hqxv7h1oaoSdS
This commit is contained in:
Claude 2026-06-22 03:14:28 +00:00
parent 701e329c2d
commit 784f360a52
No known key found for this signature in database
6 changed files with 131 additions and 58 deletions

View File

@ -8,18 +8,21 @@ pub const DIRECTION_EGRESS: u8 = 1;
/// TCP flag anomaly event written to the ring buffer by XDP programs.
///
/// align_of::<TcpAnomalyEvent>() == 8, satisfying the aya ring buffer constraint
/// that 8 % align_of::<T>() == 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,
}

View File

@ -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<u8> {
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::<TcpAnomalyEvent>(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::<TcpAnomalyEvent>(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
}

View File

@ -80,6 +80,9 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
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);

View File

@ -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<u8> {
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::<TcpAnomalyEvent>(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::<TcpAnomalyEvent>(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
}

View File

@ -82,6 +82,9 @@ unsafe fn try_access_control(ctx: &XdpContext) -> Result<u32, ()> {
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);

View File

@ -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 {