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:
parent
8b35ac693c
commit
6863249ad4
14 changed files with 972 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
182
src/xdp/globals.rs
Normal 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))
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue