feat: traffic intel hot path, SYN RST-challenge, XDP environment diagnostics
Traffic Intel (was dead code, now wired): - TrafficHook in listener accept path: cps/pps windows -> AttackDetector - auto-ban IPs below reputation threshold under attack ([detect.autoban]) - AlertDispatcher: webhook on attack state transition only (dedup), metrics AUTO_BANS_TOTAL / INTEL_* ; [detect.alert].webhook_url XDP SYN RST-challenge (Oubliette pattern, off by default): - G_SYN_CHALLENGE_ENABLED=0: kernel replies bad-ACK SYN-ACK via XDP_TX, spoofed sources stay silent, live clients answer RST with secret echo -> challenge_verified (LRU, sliding TTL); brute-force of marker impossible - maps challenge_verified/challenge_pending, STAT_CHALLENGE_*, all logic in xdp/core/syn_challenge.h (221 lines) XDP diagnostics (src/xdp/diagnostics.rs): - EnvironmentReport: kernel version/BTF/driver->AttachMode verdict, fail-fast before load on unsupported kernels; wired into CLI - SystemProbe trait for kernel-less testing fix: .gitignore 'bin/' matched src/bin/ — rampart.rs was never committed cargo build/clippy(-D warnings, --features xdp)/test green: 107 tests
This commit is contained in:
parent
40bfe956e2
commit
aa787a558c
25 changed files with 1825 additions and 28 deletions
|
|
@ -33,4 +33,12 @@ static volatile const __u64 G_UDP_WINDOW_NS = 1000000000ULL; // 1 sec
|
|||
static volatile const __u8 G_FEATURE_SYN_THROTTLE = 1;
|
||||
static volatile const __u8 G_FEATURE_EVENTS = 1;
|
||||
|
||||
// ── RST-challenge (Oubliette liveness proof) ──
|
||||
// 0 = disabled: single branch, zero cost for legitimate traffic.
|
||||
// Secret is patched by the loader at attach time (default is a placeholder).
|
||||
static volatile const __u8 G_SYN_CHALLENGE_ENABLED = 0;
|
||||
static volatile const __u64 G_CHALLENGE_SECRET = 0xA5A55A5A5A5AA5A5ULL;
|
||||
static volatile const __u32 G_CHALLENGE_TIMEOUT_MS = 3000; // pending TTL
|
||||
static volatile const __u64 G_CHALLENGE_VERIFIED_TTL_NS = 300000000000ULL; // 5 min
|
||||
|
||||
#endif /* RAMPART_CONFIG_H */
|
||||
|
|
|
|||
|
|
@ -45,6 +45,30 @@ struct {
|
|||
__type(value, struct throttle_entry);
|
||||
} udp_rate_limit SEC(".maps");
|
||||
|
||||
// ── RST-challenge pending state (challenge reflected, awaiting proof) ──
|
||||
struct challenge_pending {
|
||||
__u64 sent_at; // ktime_ns of the reflected SYN-ACK
|
||||
__u32 marker; // our ISN (client may echo it as ack)
|
||||
__u32 bad_ack; // bogus ack sent (live client echoes it as RST seq)
|
||||
};
|
||||
|
||||
// 🔗 RST-challenge: verified sources (liveness proven) — sliding TTL via
|
||||
// last_seen, LRU eviction as the hard cap
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 65536);
|
||||
__type(key, __u32); // src_ip
|
||||
__type(value, __u64); // last_seen (ktime_ns)
|
||||
} challenge_verified SEC(".maps");
|
||||
|
||||
// 🔗 RST-challenge: outstanding challenges keyed by flow 4-tuple
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 16384);
|
||||
__type(key, struct flow_key);
|
||||
__type(value, struct challenge_pending);
|
||||
} challenge_pending SEC(".maps");
|
||||
|
||||
// 🔗 Statistics (per-CPU, атомарные инкременты)
|
||||
#define STAT_TOTAL 0
|
||||
#define STAT_TCP 1
|
||||
|
|
@ -55,6 +79,9 @@ struct {
|
|||
#define STAT_DROP 6
|
||||
#define STAT_UDP 7
|
||||
#define STAT_RATE_LIMIT 8
|
||||
#define STAT_CHALLENGE_SENT 9
|
||||
#define STAT_CHALLENGE_VERIFIED 10
|
||||
#define STAT_CHALLENGE_FAILED 11
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ static __always_inline void inc_whitelist(void) { inc_stat(STAT_WHITELIST); }
|
|||
static __always_inline void inc_blacklist(void) { inc_stat(STAT_BLACKLIST); }
|
||||
static __always_inline void inc_syn_throttle(void) { inc_stat(STAT_SYN_THROTTLE); }
|
||||
static __always_inline void inc_rate_limit(void) { inc_stat(STAT_RATE_LIMIT); }
|
||||
static __always_inline void inc_chal_sent(void) { inc_stat(STAT_CHALLENGE_SENT); }
|
||||
static __always_inline void inc_chal_verified(void) { inc_stat(STAT_CHALLENGE_VERIFIED); }
|
||||
static __always_inline void inc_chal_failed(void) { inc_stat(STAT_CHALLENGE_FAILED); }
|
||||
static __always_inline void inc_pass(void) { inc_stat(STAT_PASS); }
|
||||
static __always_inline void inc_drop(void) { inc_stat(STAT_DROP); }
|
||||
|
||||
|
|
|
|||
221
xdp/core/syn_challenge.h
Normal file
221
xdp/core/syn_challenge.h
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
#ifndef RAMPART_SYN_CHALLENGE_H
|
||||
#define RAMPART_SYN_CHALLENGE_H
|
||||
|
||||
// ── Rampart RST-challenge (Oubliette-style liveness proof) ──
|
||||
//
|
||||
// Protocol-agnostic source verification, compatible with any TCP client:
|
||||
// 1. SYN from a source that is neither whitelisted nor verified:
|
||||
// kernel reflects a SYN-ACK with a DELIBERATELY WRONG ack number
|
||||
// (deterministic from the 4-tuple + secret) and drops the original SYN.
|
||||
// 2. Spoofed source never reacts — it does not own the address.
|
||||
// 3. Live client's TCP stack rejects the unacceptable SYN-ACK
|
||||
// (RFC 9293 SYN-SENT: SEG.ACK outside [ISS+1, SND.NXT] → RST)
|
||||
// echoing secret-derived values. Seeing that RST marks the source
|
||||
// verified; the app reconnects and its next SYN reaches the backend.
|
||||
//
|
||||
// Pending challenges expire after G_CHALLENGE_TIMEOUT_MS (spoof = silence).
|
||||
|
||||
#include "common.h"
|
||||
#include "config.h"
|
||||
#include "maps.h"
|
||||
#include "stats.h"
|
||||
|
||||
#define CHAL_CONTINUE (-1) // fall through to the normal filter path
|
||||
|
||||
// ── Secret-dependent 64-bit mixing (splitmix-style finalizer) ──
|
||||
static __always_inline __u64 chal_mix64(__u64 h, __u64 v)
|
||||
{
|
||||
h ^= v;
|
||||
h *= 0x9E3779B97F4A7C15ULL;
|
||||
h ^= h >> 29;
|
||||
return h;
|
||||
}
|
||||
|
||||
// Per-flow challenge values; infeasible to forge without G_CHALLENGE_SECRET
|
||||
static __always_inline __u64 chal_derive(const struct flow_key *k)
|
||||
{
|
||||
__u64 h = (__u64)G_CHALLENGE_SECRET ^ 0x5253544348414C53ULL;
|
||||
h = chal_mix64(h, ((__u64)k->src_ip << 32) | k->dst_ip);
|
||||
h = chal_mix64(h, ((__u64)k->src_port << 48) | ((__u64)k->dst_port << 32));
|
||||
h *= 0xFF51AFD7ED558CCDULL;
|
||||
h ^= h >> 32;
|
||||
return h;
|
||||
}
|
||||
|
||||
// ── Ones-complement helpers (both IPv4/TCP headers are 20 bytes = 10 words) ──
|
||||
static __always_inline __u32 chal_sum10(const __u16 *w)
|
||||
{
|
||||
__u32 s = 0;
|
||||
s += w[0]; s += w[1]; s += w[2]; s += w[3]; s += w[4];
|
||||
s += w[5]; s += w[6]; s += w[7]; s += w[8]; s += w[9];
|
||||
return s;
|
||||
}
|
||||
|
||||
static __always_inline __u16 chal_csum_fold(__u32 sum)
|
||||
{
|
||||
sum = (sum >> 16) + (sum & 0xFFFF);
|
||||
sum += sum >> 16;
|
||||
return (__u16)~sum;
|
||||
}
|
||||
|
||||
// Rewrite the packet in place into a SYN-ACK: MAC/IP/ports swapped,
|
||||
// seq = marker, ack = bad_ack (guaranteed unacceptable for the client),
|
||||
// clean 20-byte TCP header, IP checksum recomputed, TCP checksum rebuilt.
|
||||
// All reads happen BEFORE any packet write (writes invalidate verifier
|
||||
// bounds), the tail is trimmed so the peer sees no leftover option bytes.
|
||||
static __always_inline void chal_build_synack(struct xdp_md *ctx,
|
||||
struct ethhdr *eth, void *l3,
|
||||
__u8 is_ipv6, struct tcphdr *tcp,
|
||||
__u32 marker, __u32 bad_ack)
|
||||
{
|
||||
// ── Snapshot phase (packet reads) ──
|
||||
__u16 ipw[10];
|
||||
__u16 tcpw[10];
|
||||
__u8 ms[ETH_ALEN], md[ETH_ALEN];
|
||||
__u32 saddr[4] = {0}, daddr[4] = {0};
|
||||
|
||||
__builtin_memcpy(ms, eth->h_source, ETH_ALEN);
|
||||
__builtin_memcpy(md, eth->h_dest, ETH_ALEN);
|
||||
__builtin_memcpy(ipw, l3, 20);
|
||||
__builtin_memcpy(tcpw, tcp, 20);
|
||||
|
||||
if (is_ipv6) {
|
||||
__builtin_memcpy(saddr, &((struct ipv6hdr *)l3)->daddr, 16); // swapped
|
||||
__builtin_memcpy(daddr, &((struct ipv6hdr *)l3)->saddr, 16);
|
||||
} else {
|
||||
saddr[0] = ((struct iphdr *)l3)->daddr;
|
||||
daddr[0] = ((struct iphdr *)l3)->saddr;
|
||||
}
|
||||
|
||||
// ── Compute images in registers ──
|
||||
// IPv4 header: swap addresses, fresh TTL, recompute checksum
|
||||
if (!is_ipv6) {
|
||||
__u16 s0 = ipw[6], s1 = ipw[7];
|
||||
ipw[6] = ipw[8];
|
||||
ipw[7] = ipw[9];
|
||||
ipw[8] = s0;
|
||||
ipw[9] = s1;
|
||||
((__u8 *)ipw)[8] = 64; // TTL
|
||||
ipw[5] = 0; // checksum placeholder
|
||||
ipw[5] = chal_csum_fold(chal_sum10(ipw));
|
||||
}
|
||||
|
||||
// TCP header: swap ports, inject secret-derived seq/ack, SYN|ACK
|
||||
__u16 nsport = tcpw[1]; // new source = old dest
|
||||
__u16 ndport = tcpw[0]; // new dest = old source
|
||||
tcpw[0] = nsport;
|
||||
tcpw[1] = ndport;
|
||||
tcpw[2] = (__u16)(marker >> 16);
|
||||
tcpw[3] = (__u16)marker;
|
||||
tcpw[4] = (__u16)(bad_ack >> 16);
|
||||
tcpw[5] = (__u16)bad_ack;
|
||||
tcpw[6] = bpf_htons(0x5012); // doff=5, flags SYN|ACK
|
||||
tcpw[7] = bpf_htons(0xFFFF); // window
|
||||
tcpw[8] = 0; // checksum placeholder
|
||||
tcpw[9] = 0; // urg_ptr
|
||||
|
||||
// TCP checksum over pseudo-header + 20-byte header
|
||||
__u32 sum = 0;
|
||||
if (is_ipv6) {
|
||||
__u16 *w = (__u16 *)saddr;
|
||||
sum += w[0] + w[1] + w[2] + w[3] + w[4] + w[5] + w[6] + w[7];
|
||||
w = (__u16 *)daddr;
|
||||
sum += w[0] + w[1] + w[2] + w[3] + w[4] + w[5] + w[6] + w[7];
|
||||
} else {
|
||||
sum += ipw[6] + ipw[7] + ipw[8] + ipw[9]; // new IPv4 saddr/daddr
|
||||
}
|
||||
sum += bpf_htons(IPPROTO_TCP);
|
||||
sum += bpf_htons((__u16)sizeof(struct tcphdr));
|
||||
sum += chal_sum10(tcpw);
|
||||
tcpw[8] = chal_csum_fold(sum);
|
||||
|
||||
// ── Write phase ──
|
||||
__builtin_memcpy(eth->h_dest, ms, ETH_ALEN);
|
||||
__builtin_memcpy(eth->h_source, md, ETH_ALEN);
|
||||
if (is_ipv6) {
|
||||
struct ipv6hdr *ip6 = l3;
|
||||
__builtin_memcpy(&ip6->saddr, daddr, 16);
|
||||
__builtin_memcpy(&ip6->daddr, saddr, 16);
|
||||
ip6->hop_limit = 64;
|
||||
} else {
|
||||
__builtin_memcpy(l3, ipw, 20);
|
||||
}
|
||||
__builtin_memcpy(tcp, tcpw, 20);
|
||||
|
||||
// Trim to the bare header (drop SYN options/payload remnants)
|
||||
long trim = (long)((void *)(long)ctx->data_end -
|
||||
((void *)tcp + sizeof(struct tcphdr)));
|
||||
if (trim > 0)
|
||||
bpf_xdp_adjust_tail(ctx, -trim);
|
||||
}
|
||||
|
||||
// ── Pure SYN from an unknown source: reflect the wrong SYN-ACK challenge ──
|
||||
// Returns CHAL_CONTINUE (verified source — process normally), XDP_TX
|
||||
// (challenge sent) or XDP_DROP.
|
||||
static __always_inline int chal_on_syn(struct xdp_md *ctx, struct ethhdr *eth,
|
||||
void *l3, __u8 is_ipv6,
|
||||
struct tcphdr *tcp,
|
||||
const struct flow_key *flow, __u64 now)
|
||||
{
|
||||
__u64 *v = bpf_map_lookup_elem(&challenge_verified, &flow->src_ip);
|
||||
if (v) {
|
||||
if (now - *v <= G_CHALLENGE_VERIFIED_TTL_NS) {
|
||||
*v = now; // sliding TTL refresh
|
||||
return CHAL_CONTINUE;
|
||||
}
|
||||
bpf_map_delete_elem(&challenge_verified, &flow->src_ip); // expired
|
||||
}
|
||||
|
||||
__u64 h = chal_derive(flow);
|
||||
__u32 marker = (__u32)(h >> 32); // our ISN
|
||||
__u32 bad_ack = (__u32)h; // bogus ack the client must reject
|
||||
|
||||
__u32 expect = bpf_ntohl(tcp->seq) + 1;
|
||||
if (bad_ack == expect) // ~2^-32 collision with a valid handshake — kill it
|
||||
bad_ack = ~bad_ack;
|
||||
|
||||
struct challenge_pending p = {
|
||||
.sent_at = now,
|
||||
.marker = marker,
|
||||
.bad_ack = bad_ack,
|
||||
};
|
||||
if (bpf_map_update_elem(&challenge_pending, flow, &p, BPF_ANY)) {
|
||||
inc_drop(); // cannot track the challenge — fail closed
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
inc_chal_sent();
|
||||
chal_build_synack(ctx, eth, l3, is_ipv6, tcp, marker, bad_ack);
|
||||
return XDP_TX;
|
||||
}
|
||||
|
||||
// ── RST answering a pending challenge: proof of life ──
|
||||
// Accept only if the RST echoes a secret-derived value (our bad ack as its
|
||||
// seq, or our marker+1 as its ack). Anything else fails the challenge.
|
||||
static __always_inline int chal_on_rst(struct tcphdr *tcp,
|
||||
const struct flow_key *flow, __u64 now)
|
||||
{
|
||||
struct challenge_pending *p = bpf_map_lookup_elem(&challenge_pending, flow);
|
||||
if (!p)
|
||||
return CHAL_CONTINUE;
|
||||
|
||||
__u64 timeout_ns = (__u64)G_CHALLENGE_TIMEOUT_MS * 1000000ULL;
|
||||
__u32 seq = bpf_ntohl(tcp->seq);
|
||||
__u32 ack = bpf_ntohl(tcp->ack_seq);
|
||||
__u8 ok = (seq == p->bad_ack) ||
|
||||
(tcp->ack && ack == p->marker + 1);
|
||||
|
||||
bpf_map_delete_elem(&challenge_pending, flow);
|
||||
|
||||
if (!ok || now - p->sent_at > timeout_ns) {
|
||||
inc_chal_failed();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
__u64 seen = now;
|
||||
bpf_map_update_elem(&challenge_verified, &flow->src_ip, &seen, BPF_ANY);
|
||||
inc_chal_verified();
|
||||
return XDP_DROP; // client app will retry connect; next SYN passes
|
||||
}
|
||||
|
||||
#endif /* RAMPART_SYN_CHALLENGE_H */
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
#include "config.h"
|
||||
#include "stats.h"
|
||||
#include "prefix_stats.h"
|
||||
#include "syn_challenge.h"
|
||||
#include "../hooks/hook_api.h"
|
||||
|
||||
char __license[] SEC("license") = "GPL";
|
||||
|
|
@ -299,6 +300,15 @@ int rampart_universal_filter(struct xdp_md *ctx)
|
|||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// ── RST-challenge: unverified source must prove liveness first ──
|
||||
// (after throttle/blacklist, mirroring prefix_stats ordering)
|
||||
if (G_SYN_CHALLENGE_ENABLED) {
|
||||
int cv = chal_on_syn(ctx, eth, (void *)(eth + 1), is_ipv6,
|
||||
tcp, &flow, now);
|
||||
if (cv != CHAL_CONTINUE)
|
||||
return cv;
|
||||
}
|
||||
|
||||
// Count SYN only after throttle passed (don't count throttled SYNs)
|
||||
update_prefix_stats(&pkey, 1, now);
|
||||
|
||||
|
|
@ -324,6 +334,13 @@ int rampart_universal_filter(struct xdp_md *ctx)
|
|||
// pkt_count: all non-SYN TCP that survived blacklist/bypass/throttle
|
||||
update_prefix_stats(&pkey, 0, now);
|
||||
|
||||
// ── RST-challenge proof: match a pending challenge before conntrack ──
|
||||
if (G_SYN_CHALLENGE_ENABLED && tcp->rst) {
|
||||
int cv = chal_on_rst(tcp, &flow, now);
|
||||
if (cv != CHAL_CONTINUE)
|
||||
return cv;
|
||||
}
|
||||
|
||||
struct conntrack_entry *conn = bpf_map_lookup_elem(&conntrack_map, &flow);
|
||||
if (!conn) {
|
||||
// Unknown connection — drop
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue