feat: HTTP protocol plugin + XDP globals patching from config

protocol-http (compile-time feature):
- HttpHandler: incremental HTTP/1.1 request parse (fragment-safe),
  header size/timeout limits, method whitelist, Host required,
  blocked_paths, optional User-Agent requirement
- [protocol.http] config section; registry wiring behind feature

XDP globals:
- src/xdp/globals.rs: XdpGlobals + build_rodata_image() mirroring
  xdp/core/config.h layout; set via OpenMapMut::set_initial_value()
  before load ([xdp] section: ports, udp policy, throttle, challenge)
- wire set_globals into rampart binary startup

fix: gate preflight() behind xdp feature (dead code without it)
This commit is contained in:
loki5512344 2026-08-24 12:18:22 +02:00
parent 8b35ac693c
commit 6863249ad4
Signed by: boba
GPG key ID: 253067914055423B
14 changed files with 972 additions and 22 deletions

View file

@ -262,6 +262,7 @@ impl EnvironmentReport {
///
/// # Errors
/// См. [`EnvironmentReport::validate`].
#[cfg(feature = "xdp")]
pub(crate) fn preflight(interface: &str) -> Result<()> {
let report = EnvironmentReport::collect(&FilesystemProbe, interface);
let kernel = report

View file

@ -1,10 +1,11 @@
use anyhow::{Context, Result, bail};
use libbpf_rs::{MapCore, MapFlags, Object, ObjectBuilder, RingBuffer, RingBufferBuilder, Xdp, XdpFlags};
use libbpf_rs::{MapCore, MapFlags, Object, ObjectBuilder, OpenObject, RingBuffer, RingBufferBuilder, Xdp, XdpFlags};
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::os::unix::io::AsFd;
use super::XdpStats;
use super::globals::{RODATA_MAP_NAME, XdpGlobals};
use crate::traffic::prefix::{PrefixKey, PrefixStatsVal};
pub struct XdpFilter {
@ -12,6 +13,7 @@ pub struct XdpFilter {
ringbuf: Option<RingBuffer<'static>>,
ifindex: i32,
interface: String,
globals: XdpGlobals,
}
impl XdpFilter {
@ -21,19 +23,25 @@ impl XdpFilter {
ringbuf: None,
ifindex: 0,
interface: interface.to_string(),
globals: XdpGlobals::default(),
}
}
/// Задаёт глобалы для патчинга `.rodata` при следующем `load()`.
pub fn set_globals(&mut self, globals: XdpGlobals) {
self.globals = globals;
}
pub fn load(&mut self) -> Result<()> {
// Диагностика ДО загрузки: fail-fast на неподдерживаемом ядре/драйвере.
super::diagnostics::preflight(&self.interface)?;
let bpf_obj = include_bytes!(concat!(env!("OUT_DIR"), "/universal_filter.o"));
let obj = ObjectBuilder::default()
let mut open_obj = ObjectBuilder::default()
.open_memory(bpf_obj)
.context("Failed to open XDP object")?
.load()
.context("Failed to load XDP object (verifier error?)")?;
.context("Failed to open XDP object")?;
patch_rodata(&mut open_obj, &self.globals)?;
let obj = open_obj.load().context("Failed to load XDP object (verifier error?)")?;
let ifindex = unsafe { libc::if_nametoindex(self.interface.as_ptr() as *const libc::c_char) };
if ifindex == 0 {
@ -182,6 +190,17 @@ fn unix_ns() -> u64 {
.as_nanos() as u64
}
/// Патчит карту `rampart_.rodata` (volatile const глобалы) ДО загрузки объекта.
fn patch_rodata(open_obj: &mut OpenObject, globals: &XdpGlobals) -> Result<()> {
let image = globals.build_rodata_image();
let mut map = open_obj
.maps_mut()
.find(|m| m.name() == RODATA_MAP_NAME)
.with_context(|| format!("map '{RODATA_MAP_NAME}' not found in XDP object"))?;
map.set_initial_value(&image)
.with_context(|| format!("failed to set initial value of '{RODATA_MAP_NAME}'"))
}
fn build_ringbuf(obj: &Object) -> Result<RingBuffer<'static>> {
let map = obj
.maps()

182
src/xdp/globals.rs Normal file
View file

@ -0,0 +1,182 @@
//! Патчинг volatile const глобалов XDP-программы (`xdp/core/config.h`) из TOML-конфига.
//!
//! Глобалы живут в секции `.rodata`, которая при загрузке превращается в карту
//! с именем `rampart_.rodata`. Образ патчится до `load()` через
//! `OpenMapMut::set_initial_value()` (см. [`crate::xdp::filter`]).
//!
//! # Схема маппинга `[xdp]` → G_* (единственный источник истины)
//!
//! | Конфиг | Глобал | Тип |
//! |-------------------------|------------------------------|-----|
//! | `protected_port_start` | `G_START_PORT` | u16 |
//! | `protected_port_end` | `G_END_PORT` | u16 |
//! | `udp_policy` | `G_UDP_POLICY` | u8 ("pass"=0, "drop"=1, "rate-limit"=2) |
//! | `udp_rate_hit_count` | `G_UDP_HIT_COUNT` | u32 |
//! | `udp_rate_window_ms` | `G_UDP_WINDOW_NS` | u64 (мс → нс) |
//! | `throttle_enabled` | `G_FEATURE_SYN_THROTTLE` | u8 (bool → 0/1) |
//! | `events_enabled` | `G_FEATURE_EVENTS` | u8 (bool → 0/1) |
//! | `syn_challenge_enabled` | `G_SYN_CHALLENGE_ENABLED` | u8 (bool → 0/1) |
//! | `challenge_secret_hex` | `G_CHALLENGE_SECRET` | u64 (hex, big-endian) |
//! | `challenge_timeout_ms` | `G_CHALLENGE_TIMEOUT_MS` | u32 |
//!
//! Остальные глобалы (`G_SYN_*`, `G_CONNTRACK_IDLE_NS`, `G_PLAYER_IDLE_NS`,
//! `G_BAN_DURATION_NS`, `G_MAX_OUT_OF_ORDER`, `G_CHALLENGE_VERIFIED_TTL_NS`)
//! конфигом пока не управляются и патчатся значениями по умолчанию из config.h.
use anyhow::{Context, Result, bail};
use crate::config::XdpConfig;
/// Имя карты с образом `.rodata` загруженного BPF-объекта.
pub(crate) const RODATA_MAP_NAME: &str = "rampart_.rodata";
/// Размер образа `.rodata` в байтах (см. `build_rodata_image`).
pub const RODATA_SIZE: usize = 96;
/// Значение `G_UDP_POLICY`: пропускать UDP.
pub const UDP_POLICY_PASS: u8 = 0;
/// Значение `G_UDP_POLICY`: дропать весь UDP.
pub const UDP_POLICY_DROP: u8 = 1;
/// Значение `G_UDP_POLICY`: rate-limit по окну/порогу.
pub const UDP_POLICY_RATE_LIMIT: u8 = 2;
/// Дефолт-плейсхолдер `G_CHALLENGE_SECRET` из config.h.
const DEFAULT_CHALLENGE_SECRET: u64 = 0xA5A5_A5A5_A5A5_A5A5;
/// Runtime-значения глобалов XDP-программы; `Default` совпадает с config.h.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XdpGlobals {
pub start_port: u16,
pub end_port: u16,
pub syn_hit_count: u32,
pub syn_window_ns: u64,
pub syn_ban_duration_ns: u64,
pub conntrack_idle_ns: u64,
pub player_idle_ns: u64,
pub ban_duration_ns: u64,
pub max_out_of_order: u8,
pub udp_policy: u8,
pub udp_hit_count: u32,
pub udp_window_ns: u64,
pub feature_syn_throttle: bool,
pub feature_events: bool,
pub syn_challenge_enabled: bool,
pub challenge_secret: u64,
pub challenge_timeout_ms: u32,
pub challenge_verified_ttl_ns: u64,
}
impl Default for XdpGlobals {
fn default() -> Self {
Self {
start_port: 1,
end_port: 65535,
syn_hit_count: 10,
syn_window_ns: 3_000_000_000,
syn_ban_duration_ns: 60_000_000_000,
conntrack_idle_ns: 30_000_000_000,
player_idle_ns: 120_000_000_000,
ban_duration_ns: 300_000_000_000,
max_out_of_order: 4,
udp_policy: UDP_POLICY_PASS,
udp_hit_count: 100,
udp_window_ns: 1_000_000_000,
feature_syn_throttle: true,
feature_events: true,
syn_challenge_enabled: false,
challenge_secret: DEFAULT_CHALLENGE_SECRET,
challenge_timeout_ms: 3000,
challenge_verified_ttl_ns: 300_000_000_000,
}
}
}
impl XdpGlobals {
/// Собирает байтовый образ `.rodata` в порядке объявления глобалов
/// в `xdp/core/config.h` с естественным выравниванием каждого поля.
///
/// RISK: раскладка предполагает, что линкер кладёт статические переменные
/// в секцию `.rodata` в порядке объявления с natural alignment — на
/// практике это так (каждая `static volatile const` получает своё
/// выравнивание, padding виден в смещениях ниже), но стандарт C этого не
/// гарантирует. TODO: верифицировать смещения на сервере через BTF-dump
/// карты `rampart_.rodata` (`bpftool btf dump`) и зафиксировать их здесь.
pub fn build_rodata_image(&self) -> Vec<u8> {
let mut img = vec![0u8; RODATA_SIZE];
// u16 ×2 без padding: смещения 0..4.
img[0..2].copy_from_slice(&self.start_port.to_le_bytes());
img[2..4].copy_from_slice(&self.end_port.to_le_bytes());
// u32, align 4: 4..8.
img[4..8].copy_from_slice(&self.syn_hit_count.to_le_bytes());
// u64, align 8: 8..48 (SYN throttle + idle timeouts + ban duration).
img[8..16].copy_from_slice(&self.syn_window_ns.to_le_bytes());
img[16..24].copy_from_slice(&self.syn_ban_duration_ns.to_le_bytes());
img[24..32].copy_from_slice(&self.conntrack_idle_ns.to_le_bytes());
img[32..40].copy_from_slice(&self.player_idle_ns.to_le_bytes());
img[40..48].copy_from_slice(&self.ban_duration_ns.to_le_bytes());
// u8 ×3 подряд: 48, 49, 50 (max_out_of_order, udp_policy, —).
img[48] = self.max_out_of_order;
img[49] = self.udp_policy;
// u32 после u8: padding 50..52, значение 52..56.
img[52..56].copy_from_slice(&self.udp_hit_count.to_le_bytes());
// u64, align 8: 56..64.
img[56..64].copy_from_slice(&self.udp_window_ns.to_le_bytes());
// u8 ×3 (feature flags + challenge flag): 64, 65, 66.
img[64] = u8::from(self.feature_syn_throttle);
img[65] = u8::from(self.feature_events);
img[66] = u8::from(self.syn_challenge_enabled);
// u64 после u8: padding 67..72, значение 72..80.
img[72..80].copy_from_slice(&self.challenge_secret.to_le_bytes());
// u32: 80..84.
img[80..84].copy_from_slice(&self.challenge_timeout_ms.to_le_bytes());
// u64 после u32: padding 84..88, значение 88..96.
img[88..96].copy_from_slice(&self.challenge_verified_ttl_ns.to_le_bytes());
img
}
/// Строит глобалы из секции `[xdp]` конфига.
///
/// # Errors
/// Неизвестное значение `udp_policy`; некорректный `challenge_secret_hex`
/// (не hex или не 8 байт); `protected_port_end < protected_port_start`.
pub fn from_config(cfg: &XdpConfig) -> Result<Self> {
let mut g = Self {
start_port: cfg.protected_port_start,
end_port: cfg.protected_port_end,
udp_hit_count: cfg.udp_rate_hit_count,
udp_window_ns: cfg.udp_rate_window_ms.saturating_mul(1_000_000),
feature_syn_throttle: cfg.throttle_enabled,
feature_events: cfg.events_enabled,
syn_challenge_enabled: cfg.syn_challenge_enabled,
challenge_timeout_ms: cfg.challenge_timeout_ms,
..Self::default()
};
if g.end_port < g.start_port {
bail!(
"xdp.protected_port_end ({}) < xdp.protected_port_start ({})",
g.end_port,
g.start_port
);
}
g.udp_policy = match cfg.udp_policy.as_str() {
"pass" => UDP_POLICY_PASS,
"drop" => UDP_POLICY_DROP,
"rate-limit" => UDP_POLICY_RATE_LIMIT,
other => bail!("invalid xdp.udp_policy '{other}' (expected pass|drop|rate-limit)"),
};
if let Some(secret) = cfg.challenge_secret_hex.as_deref() {
g.challenge_secret = parse_secret_hex(secret)?;
}
Ok(g)
}
}
/// Парсит 128-битный hex (16 символов, опциональный префикс `0x`) в u64 (big-endian).
fn parse_secret_hex(s: &str) -> Result<u64> {
let cleaned = s.trim().trim_start_matches("0x").replace(['_', ':', '-'], "");
let bytes = hex::decode(&cleaned).with_context(|| format!("invalid xdp.challenge_secret_hex '{s}'"))?;
let arr: [u8; 8] = bytes
.try_into()
.map_err(|v: Vec<u8>| anyhow::anyhow!("xdp.challenge_secret_hex must be exactly 8 bytes, got {}", v.len()))?;
Ok(u64::from_be_bytes(arr))
}

View file

@ -7,6 +7,11 @@ pub use diagnostics::{
AttachMode, EnvironmentReport, FilesystemProbe, KernelVersion, MIN_KERNEL, SystemProbe, driver_supports_native_xdp,
};
#[cfg(feature = "xdp")]
mod globals;
#[cfg(feature = "xdp")]
pub use globals::XdpGlobals;
#[cfg(feature = "xdp")]
mod filter;
#[cfg(feature = "xdp")]