feat!: universal redesign — drop Minecraft stack, single-crate architecture
- remove Java plugins (velocity/paper), dashboard, all MC-specific code
(handshake, death_code, varint, hostname-HMAC); available in history pre-v0.2
- merge crates/* into one package with src/bin/{rampart,rampart-manager,rampart-cli}
- ProtocolHandler trait + registry (no implementations yet), universal PoW kept
- XDP: universal L3/L4 filter (xdp/core/) + pluggable hook API (xdp/hooks/),
fix IPv6 saddr bug; clang build verified
- docs: bilingual knowledge base (docs/kb/: attacks x4, defense-levels,
practice x3), rewrite README/architecture for universal concept
- TODO.md v4.0: <=300-line module limit, competitor benchmark section (ref/)
- deploy/CI/docs cleanup: no MC references, new binary names
cargo build/clippy(-D warnings)/test green (55 tests)
This commit is contained in:
parent
0b53ed720b
commit
15f474486a
179 changed files with 5044 additions and 11519 deletions
|
|
@ -8,20 +8,14 @@
|
|||
#define ETH_P_IP 0x0800
|
||||
#define ETH_P_IPV6 0x86DD
|
||||
#define IPPROTO_TCP 6
|
||||
#define IPPROTO_UDP 17
|
||||
#define IP_OFFSET 0x1FFF
|
||||
#define IP_MF 0x2000
|
||||
|
||||
#define MC_PORT_MIN 25565
|
||||
#define MC_PORT_MAX 25570
|
||||
|
||||
// ── TCP state machine ──
|
||||
// ── Protocol-agnostic TCP state machine ──
|
||||
enum connection_state {
|
||||
STATE_AWAIT_ACK = 0,
|
||||
STATE_AWAIT_MC_HANDSHAKE = 1,
|
||||
STATE_AWAIT_LOGIN = 2,
|
||||
STATE_VERIFIED = 3,
|
||||
STATE_PING_SENT = 4,
|
||||
STATE_PING_COMPLETE = 5,
|
||||
STATE_SYN_RECEIVED = 0, // SYN seen, waiting for ACK (handshake completing)
|
||||
STATE_ESTABLISHED = 1, // handshake done; payload handed to proto_hook()
|
||||
};
|
||||
|
||||
// ── Flow key (4-tuple for connection tracking) ──
|
||||
|
|
@ -33,24 +27,18 @@ struct flow_key {
|
|||
};
|
||||
|
||||
// ── Connection tracking entry ──
|
||||
// Timer-based cleanup not needed: LRU maps handle eviction automatically.
|
||||
// Stale entries are evicted when map is full.
|
||||
// LRU maps evict stale entries automatically when full; RST/FIN removes
|
||||
// entries explicitly. last_seen supports future timer-based idle cleanup.
|
||||
struct conntrack_entry {
|
||||
__u32 state;
|
||||
__u32 expected_seq;
|
||||
__u32 src_ip;
|
||||
__u32 protocol;
|
||||
__u64 last_seen;
|
||||
__u16 src_port;
|
||||
__u8 fails;
|
||||
};
|
||||
|
||||
// ── Verified player entry ──
|
||||
struct player_entry {
|
||||
__u32 packets;
|
||||
__u32 protocol;
|
||||
};
|
||||
|
||||
// ── SYN throttle entry ──
|
||||
// ── SYN / UDP throttle entry ──
|
||||
struct throttle_entry {
|
||||
__u64 window_start;
|
||||
__u32 hits;
|
||||
|
|
@ -66,9 +54,8 @@ struct lpm_key {
|
|||
enum event_type {
|
||||
EVENT_BAN = 0,
|
||||
EVENT_RATE_LIMIT = 1,
|
||||
EVENT_DEATH_CODE = 2,
|
||||
EVENT_VERIFIED = 3,
|
||||
EVENT_CONN_DROP = 4,
|
||||
EVENT_POLICY_DROP = 2,
|
||||
EVENT_CONN_DROP = 3,
|
||||
};
|
||||
|
||||
struct xdp_event {
|
||||
|
|
@ -24,6 +24,11 @@ static volatile const __u64 G_BAN_DURATION_NS = 300000000000ULL; // 5 min
|
|||
// ── Max out-of-order packets before dropping connection ──
|
||||
static volatile const __u8 G_MAX_OUT_OF_ORDER = 4;
|
||||
|
||||
// ── UDP policy ──
|
||||
static volatile const __u8 G_UDP_POLICY = 0; // 0 = pass, 1 = drop, 2 = rate-limit
|
||||
static volatile const __u32 G_UDP_HIT_COUNT = 100; // max packets / window
|
||||
static volatile const __u64 G_UDP_WINDOW_NS = 1000000000ULL; // 1 sec
|
||||
|
||||
// ── Feature flags ──
|
||||
static volatile const __u8 G_FEATURE_SYN_THROTTLE = 1;
|
||||
static volatile const __u8 G_FEATURE_EVENTS = 1;
|
||||
|
|
@ -20,9 +20,8 @@ struct {
|
|||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
} whitelist_map SEC(".maps");
|
||||
|
||||
// 🔗 Connection tracking (unverified connections)
|
||||
// 🔗 Connection tracking (SYN_RECEIVED → ESTABLISHED)
|
||||
// LRU — автоматическое вытеснение старых записей
|
||||
// bpf_timer — idle cleanup через 30 сек
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 16384);
|
||||
|
|
@ -30,14 +29,6 @@ struct {
|
|||
__type(value, struct conntrack_entry);
|
||||
} conntrack_map SEC(".maps");
|
||||
|
||||
// 🔗 Verified player connections (LRU)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 65535);
|
||||
__type(key, struct flow_key);
|
||||
__type(value, struct player_entry);
|
||||
} player_connection_map SEC(".maps");
|
||||
|
||||
// 🔗 SYN throttle per-source-IP (LRU)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
|
|
@ -46,15 +37,24 @@ struct {
|
|||
__type(value, struct throttle_entry);
|
||||
} connection_throttle SEC(".maps");
|
||||
|
||||
// 🔗 UDP rate limit per-source-IP (LRU) — policy from config.h
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||||
__uint(max_entries, 65535);
|
||||
__type(key, __u32); // src_ip
|
||||
__type(value, struct throttle_entry);
|
||||
} udp_rate_limit SEC(".maps");
|
||||
|
||||
// 🔗 Statistics (per-CPU, атомарные инкременты)
|
||||
#define STAT_TOTAL 0
|
||||
#define STAT_TCP_MC 1
|
||||
#define STAT_WHITELIST 2
|
||||
#define STAT_BLACKLIST 3
|
||||
#define STAT_TOTAL 0
|
||||
#define STAT_TCP 1
|
||||
#define STAT_WHITELIST 2
|
||||
#define STAT_BLACKLIST 3
|
||||
#define STAT_SYN_THROTTLE 4
|
||||
#define STAT_PASS 5
|
||||
#define STAT_DROP 6
|
||||
#define STAT_VERIFIED 7
|
||||
#define STAT_PASS 5
|
||||
#define STAT_DROP 6
|
||||
#define STAT_UDP 7
|
||||
#define STAT_RATE_LIMIT 8
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
|
||||
26
xdp/core/stats.h
Normal file
26
xdp/core/stats.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef RAMPART_STATS_H
|
||||
#define RAMPART_STATS_H
|
||||
|
||||
#include "common.h"
|
||||
#include "maps.h"
|
||||
|
||||
// ── Per-CPU stat increment ──
|
||||
static __always_inline void inc_stat(__u32 idx)
|
||||
{
|
||||
__u64 *val = bpf_map_lookup_elem(&stats_map, &idx);
|
||||
if (val)
|
||||
__sync_fetch_and_add(val, 1);
|
||||
}
|
||||
|
||||
// ── Convenience wrappers ──
|
||||
static __always_inline void inc_total(void) { inc_stat(STAT_TOTAL); }
|
||||
static __always_inline void inc_tcp(void) { inc_stat(STAT_TCP); }
|
||||
static __always_inline void inc_udp(void) { inc_stat(STAT_UDP); }
|
||||
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_pass(void) { inc_stat(STAT_PASS); }
|
||||
static __always_inline void inc_drop(void) { inc_stat(STAT_DROP); }
|
||||
|
||||
#endif /* RAMPART_STATS_H */
|
||||
398
xdp/core/universal_filter.c
Normal file
398
xdp/core/universal_filter.c
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
// ── Rampart Universal XDP Filter ──
|
||||
// Protocol-agnostic L3/L4 filter with an L7 extension point.
|
||||
//
|
||||
// Responsibilities (L3/L4):
|
||||
// 1. No pure ACK drop (prevents TCP handshake deadlock)
|
||||
// 2. LRU conntrack: SYN_RECEIVED → ESTABLISHED, seq tracking
|
||||
// 3. RST/FIN removes conntrack entry (no stale state)
|
||||
// 4. IPv6 support alongside IPv4
|
||||
// 5. IP/CIDR blacklist + whitelist (LPM_TRIE)
|
||||
// 6. SYN throttle per-IP, invalid TCP flag combos dropped
|
||||
// 7. UDP policy from config.h: pass / drop / rate-limit
|
||||
//
|
||||
// Deep payload inspection is delegated to an optional protocol hook:
|
||||
// see ../hooks/hook_api.h. Without a hook, established connections pass.
|
||||
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/ipv6.h>
|
||||
#include <linux/tcp.h>
|
||||
#include <linux/udp.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "maps.h"
|
||||
#include "config.h"
|
||||
#include "stats.h"
|
||||
#include "../hooks/hook_api.h"
|
||||
|
||||
char __license[] SEC("license") = "GPL";
|
||||
|
||||
// ── TCP flag check (drop malicious combos) ──
|
||||
// Returns 1 if packet should be dropped
|
||||
static __always_inline __u8 detect_tcp_bypass(struct tcphdr *tcp)
|
||||
{
|
||||
__u8 flags = *((__u8 *)tcp + 13);
|
||||
|
||||
// No flags at all — bogus packet
|
||||
if ((flags & 0x3F) == 0)
|
||||
return 1;
|
||||
|
||||
// SYN+FIN or SYN+RST — always forged
|
||||
if (tcp->syn) {
|
||||
if (tcp->fin || tcp->rst)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SYN+ACK from client side — only servers send this
|
||||
if (tcp->syn && tcp->ack)
|
||||
return 1;
|
||||
|
||||
// URG flag — not used by any mainstream protocol on protected ports
|
||||
if (tcp->urg)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Blacklist lookup with expiry; removes expired entries ──
|
||||
static __always_inline __u8 is_blacklisted(__u32 src_ip, __u64 now)
|
||||
{
|
||||
struct lpm_key key = { .prefixlen = 32, .ip = src_ip };
|
||||
__u64 *ban_until = bpf_map_lookup_elem(&blacklist_map, &key);
|
||||
if (!ban_until)
|
||||
return 0;
|
||||
if (*ban_until > now)
|
||||
return 1;
|
||||
bpf_map_delete_elem(&blacklist_map, &key); // expired — clean up
|
||||
return 0;
|
||||
}
|
||||
|
||||
static __always_inline void ban_ip(__u32 src_ip, __u64 until)
|
||||
{
|
||||
struct lpm_key key = { .prefixlen = 32, .ip = src_ip };
|
||||
bpf_map_update_elem(&blacklist_map, &key, &until, BPF_ANY);
|
||||
}
|
||||
|
||||
// ── Generic per-IP window throttle (shared by SYN throttle and UDP RL) ──
|
||||
// map: LRU_HASH keyed by src_ip; returns 1 if limit exceeded (caller bans/drops)
|
||||
static __always_inline __u8 throttle_hit(void *map, __u32 src_ip,
|
||||
__u32 max_hits, __u64 window_ns,
|
||||
__u64 now)
|
||||
{
|
||||
struct throttle_entry *th = bpf_map_lookup_elem(map, &src_ip);
|
||||
|
||||
if (th) {
|
||||
if (now - th->window_start < window_ns) {
|
||||
if (th->hits >= max_hits)
|
||||
return 1;
|
||||
__sync_fetch_and_add(&th->hits, 1);
|
||||
} else {
|
||||
th->window_start = now;
|
||||
th->hits = 1;
|
||||
}
|
||||
} else {
|
||||
struct throttle_entry new_th = { .window_start = now, .hits = 1 };
|
||||
bpf_map_update_elem(map, &src_ip, &new_th, BPF_ANY);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// Main XDP entry point
|
||||
// ══════════════════════════════════════════
|
||||
SEC("xdp")
|
||||
int rampart_universal_filter(struct xdp_md *ctx)
|
||||
{
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
void *data = (void *)(long)ctx->data;
|
||||
|
||||
inc_total();
|
||||
|
||||
// ── Parse Ethernet header ──
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
// Non-IP traffic: pass (ARP, etc.)
|
||||
if (eth->h_proto != bpf_htons(ETH_P_IP) && eth->h_proto != bpf_htons(ETH_P_IPV6))
|
||||
return XDP_PASS;
|
||||
|
||||
// ── Parse IP header ──
|
||||
__u32 src_ip = 0;
|
||||
__u32 dst_ip = 0;
|
||||
__u8 l4_proto = 0;
|
||||
__u8 is_ipv6 = 0;
|
||||
|
||||
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
|
||||
struct iphdr *ip = (void *)(eth + 1);
|
||||
if ((void *)(ip + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
if (ip->ihl < 5)
|
||||
return XDP_DROP;
|
||||
|
||||
if (ip->protocol != IPPROTO_TCP && ip->protocol != IPPROTO_UDP)
|
||||
return XDP_PASS;
|
||||
|
||||
// Non-sequential fragment: pass (can't inspect ports safely)
|
||||
if (ip->frag_off & bpf_htons(IP_OFFSET))
|
||||
return XDP_PASS;
|
||||
|
||||
// First fragment with MF: drop (can't reassemble)
|
||||
if (ip->frag_off & bpf_htons(IP_MF))
|
||||
return XDP_DROP;
|
||||
|
||||
src_ip = ip->saddr;
|
||||
dst_ip = ip->daddr;
|
||||
l4_proto = ip->protocol;
|
||||
} else {
|
||||
struct ipv6hdr *ip6 = (void *)(eth + 1);
|
||||
if ((void *)(ip6 + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
if (ip6->nexthdr != IPPROTO_TCP && ip6->nexthdr != IPPROTO_UDP)
|
||||
return XDP_PASS; // extension headers not walked
|
||||
|
||||
// Use IPv4-mapped IPv6 address for flow key (::ffff:0:0/96)
|
||||
if (ip6->saddr.in6_u.u6_addr32[0] != 0 ||
|
||||
ip6->saddr.in6_u.u6_addr32[1] != 0 ||
|
||||
ip6->saddr.in6_u.u6_addr32[2] != bpf_htonl(0xFFFF) ||
|
||||
ip6->daddr.in6_u.u6_addr32[0] != 0 ||
|
||||
ip6->daddr.in6_u.u6_addr32[1] != 0 ||
|
||||
ip6->daddr.in6_u.u6_addr32[2] != bpf_htonl(0xFFFF)) {
|
||||
// Non-mapped IPv6 — pass for now (not supported)
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// FIX: source address must come from saddr, never daddr
|
||||
src_ip = ip6->saddr.in6_u.u6_addr32[3];
|
||||
dst_ip = ip6->daddr.in6_u.u6_addr32[3];
|
||||
l4_proto = ip6->nexthdr;
|
||||
is_ipv6 = 1;
|
||||
}
|
||||
|
||||
// ── Parse L4 header ──
|
||||
__u8 ip_hdr_len;
|
||||
void *l4;
|
||||
|
||||
if (is_ipv6) {
|
||||
struct ipv6hdr *ip6 = (void *)(eth + 1);
|
||||
l4 = (void *)(ip6 + 1);
|
||||
ip_hdr_len = sizeof(struct ipv6hdr);
|
||||
} else {
|
||||
struct iphdr *ip = (void *)(eth + 1);
|
||||
l4 = (void *)ip + (ip->ihl * 4);
|
||||
ip_hdr_len = ip->ihl * 4;
|
||||
}
|
||||
|
||||
if ((void *)l4 + sizeof(struct tcphdr) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
// ── Port range check (protected services) ──
|
||||
__u16 dst_port;
|
||||
if (l4_proto == IPPROTO_TCP)
|
||||
dst_port = bpf_ntohs(((struct tcphdr *)l4)->dest);
|
||||
else
|
||||
dst_port = bpf_ntohs(((struct udphdr *)l4)->dest);
|
||||
|
||||
if (dst_port < G_START_PORT || dst_port > G_END_PORT)
|
||||
return XDP_PASS;
|
||||
|
||||
if (l4_proto == IPPROTO_TCP)
|
||||
inc_tcp();
|
||||
else
|
||||
inc_udp();
|
||||
|
||||
__u64 now = bpf_ktime_get_ns();
|
||||
|
||||
// ── Whitelist check (CIDR) — before any filtering ──
|
||||
struct lpm_key wl_key = { .prefixlen = 32, .ip = src_ip };
|
||||
if (bpf_map_lookup_elem(&whitelist_map, &wl_key)) {
|
||||
inc_whitelist();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// ── Blacklist check (CIDR with expiry) ──
|
||||
if (is_blacklisted(src_ip, now)) {
|
||||
inc_blacklist();
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_BAN, src_ip, 0);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// ═══════════ UDP path ═══════════
|
||||
if (l4_proto == IPPROTO_UDP) {
|
||||
switch (G_UDP_POLICY) {
|
||||
case 1: // drop
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_POLICY_DROP, src_ip, 0);
|
||||
return XDP_DROP;
|
||||
case 2: // rate-limit
|
||||
if (throttle_hit(&udp_rate_limit, src_ip,
|
||||
G_UDP_HIT_COUNT, G_UDP_WINDOW_NS, now)) {
|
||||
inc_rate_limit();
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_RATE_LIMIT, src_ip, G_UDP_HIT_COUNT);
|
||||
return XDP_DROP;
|
||||
}
|
||||
/* fallthrough: within limits — pass */
|
||||
default: // pass
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════ TCP path ═══════════
|
||||
struct tcphdr *tcp = l4;
|
||||
|
||||
// Malicious TCP flags
|
||||
if (detect_tcp_bypass(tcp)) {
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_POLICY_DROP, src_ip, 0);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// TCP header length check
|
||||
if (tcp->doff < 5)
|
||||
return XDP_DROP;
|
||||
__u8 tcp_hdr_len = tcp->doff * 4;
|
||||
if ((void *)data + sizeof(struct ethhdr) + ip_hdr_len + tcp_hdr_len > data_end)
|
||||
return XDP_DROP;
|
||||
|
||||
// Compute payload pointers
|
||||
__u8 *tcp_payload = (__u8 *)tcp + tcp_hdr_len;
|
||||
__u8 *tcp_payload_end = (__u8 *)data_end;
|
||||
__s32 tcp_payload_len = (__s32)(tcp_payload_end - tcp_payload);
|
||||
if (tcp_payload_len < 0)
|
||||
tcp_payload_len = 0;
|
||||
|
||||
// ── Build flow key ──
|
||||
struct flow_key flow = {
|
||||
.src_ip = src_ip,
|
||||
.dst_ip = dst_ip,
|
||||
.src_port = tcp->source,
|
||||
.dst_port = tcp->dest,
|
||||
};
|
||||
|
||||
// ── SYN handling (new connection) ──
|
||||
if (tcp->syn && !tcp->ack) {
|
||||
// SYN throttle per-IP
|
||||
if (G_FEATURE_SYN_THROTTLE &&
|
||||
throttle_hit(&connection_throttle, src_ip,
|
||||
G_SYN_HIT_COUNT, G_SYN_WINDOW_NS, now)) {
|
||||
ban_ip(src_ip, now + G_SYN_BAN_DURATION_NS);
|
||||
inc_syn_throttle();
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_RATE_LIMIT, src_ip, G_SYN_HIT_COUNT);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Create conntrack entry for new connection
|
||||
struct conntrack_entry ce = {};
|
||||
ce.state = STATE_SYN_RECEIVED;
|
||||
ce.expected_seq = bpf_ntohl(tcp->seq) + 1; // expect ACK seq
|
||||
ce.src_ip = src_ip;
|
||||
ce.src_port = tcp->source;
|
||||
ce.last_seen = now;
|
||||
|
||||
if (bpf_map_update_elem(&conntrack_map, &flow, &ce, BPF_ANY)) {
|
||||
// Map full — should not happen with LRU
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
inc_pass();
|
||||
return XDP_PASS; // Let SYN through
|
||||
}
|
||||
|
||||
// ── Connection tracking lookup ──
|
||||
struct conntrack_entry *conn = bpf_map_lookup_elem(&conntrack_map, &flow);
|
||||
if (!conn) {
|
||||
// Unknown connection — drop
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
conn->last_seen = now;
|
||||
|
||||
// Handle RST/FIN — clean up conntrack entry
|
||||
if (tcp->rst || tcp->fin) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// ── Sequence number tracking (established connections only) ──
|
||||
__u32 seq = bpf_ntohl(tcp->seq);
|
||||
if (conn->state == STATE_ESTABLISHED && tcp_payload_len > 0 &&
|
||||
seq != conn->expected_seq) {
|
||||
conn->fails++;
|
||||
if (conn->fails >= G_MAX_OUT_OF_ORDER) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_CONN_DROP, src_ip, conn->fails);
|
||||
return XDP_DROP;
|
||||
}
|
||||
// Allow retransmission (don't update expected_seq)
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// ── State machine ──
|
||||
if (conn->state == STATE_SYN_RECEIVED) {
|
||||
// Expecting the ACK that completes the 3-way handshake
|
||||
if (!tcp->ack || seq != conn->expected_seq) {
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Do NOT drop pure ACK here. Other XDP filters drop the pure ACK
|
||||
// expecting the data packet to serve as ACK, which causes ~1-7s
|
||||
// TCP handshake deadlock. We pass the ACK through and promote
|
||||
// the connection to ESTABLISHED.
|
||||
conn->state = STATE_ESTABLISHED;
|
||||
conn->expected_seq = seq + tcp_payload_len;
|
||||
|
||||
if (tcp_payload_len == 0) {
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
// Client sent data together with the final ACK — fall through to hook
|
||||
}
|
||||
|
||||
// STATE_ESTABLISHED: empty segments pass untouched (keep-alives),
|
||||
// payload segments go through the optional protocol hook.
|
||||
if (tcp_payload_len > 0) {
|
||||
struct cursor c = { .pos = tcp_payload, .end = tcp_payload_end };
|
||||
struct pkt_meta m = {
|
||||
.src_ip = src_ip,
|
||||
.dst_ip = dst_ip,
|
||||
.src_port = bpf_ntohs(tcp->source),
|
||||
.dst_port = dst_port,
|
||||
.payload_len = (__u16)tcp_payload_len,
|
||||
.tcp_flags = *((__u8 *)tcp + 13),
|
||||
.is_ipv6 = is_ipv6,
|
||||
};
|
||||
|
||||
enum xdp_action verdict = proto_hook(&c, &m);
|
||||
|
||||
if (verdict == XDP_DROP) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_CONN_DROP, src_ip, 0);
|
||||
return XDP_DROP;
|
||||
}
|
||||
conn->expected_seq = seq + tcp_payload_len;
|
||||
}
|
||||
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
121
xdp/hooks/hook_api.h
Normal file
121
xdp/hooks/hook_api.h
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#ifndef RAMPART_HOOK_API_H
|
||||
#define RAMPART_HOOK_API_H
|
||||
|
||||
/*
|
||||
* ── Rampart protocol-hook contract ──
|
||||
*
|
||||
* The universal XDP filter is protocol-agnostic: it validates L2/L3/L4,
|
||||
* tracks the TCP handshake (SYN_RECEIVED → ESTABLISHED), applies
|
||||
* blacklist/whitelist/throttle policy — and then delegates deep payload
|
||||
* inspection to an optional protocol hook.
|
||||
*
|
||||
* ── WHAT THE HOOK SEES ──
|
||||
*
|
||||
* proto_hook() is called ONLY when:
|
||||
* - packet is TCP, destination port is inside the protected range;
|
||||
* - connection passed SYN throttle and completed the 3-way handshake
|
||||
* (state == STATE_ESTABLISHED);
|
||||
* - the segment carries a non-zero payload.
|
||||
*
|
||||
* Pure SYN / SYN-ACK / ACK (handshake segments) NEVER reach the hook:
|
||||
* the hook physically cannot block a clean TCP handshake.
|
||||
* Empty ACKs also never reach the hook (keep-alives pass untouched).
|
||||
*
|
||||
* struct cursor — bounds-checked window over the TCP payload:
|
||||
* pos = first payload byte, end = one past the last
|
||||
* byte of the packet (validated against data_end).
|
||||
* struct pkt_meta — read-only metadata: 4-tuple (ports in HOST byte
|
||||
* order), payload length, full TCP flags byte,
|
||||
* L3 family.
|
||||
*
|
||||
* ── VERDICTS ──
|
||||
*
|
||||
* XDP_PASS — accept the segment; filter advances expected_seq by
|
||||
* the consumed payload length and keeps conntrack entry.
|
||||
* XDP_DROP — drop the segment AND tear down the conntrack entry.
|
||||
* Anything else (XDP_ABORTED etc.) is treated as XDP_PASS; do not
|
||||
* return it.
|
||||
*
|
||||
* ── RULES FOR HOOK IMPLEMENTATIONS ──
|
||||
*
|
||||
* 1. Never block handshake traffic — guaranteed by design (see above),
|
||||
* do not try to work around it either.
|
||||
* 2. Bounded loops only: every loop must have a compile-time-constant
|
||||
* trip count (or be provably bounded for the verifier), keep total
|
||||
* complexity low — the program must still load with unrolled loops.
|
||||
* 3. Every cursor advance MUST be bounds-checked against c->end
|
||||
* (use cursor_take()); reading past c->end is forbidden even if it
|
||||
* seems in-bounds in the packet.
|
||||
* 4. Do not update conntrack/throttle/blacklist maps from the hook;
|
||||
* emit events via push_event() if needed (ringbuf, may fail silently).
|
||||
* 5. Keep the hook stateless per-call: all per-connection state lives
|
||||
* in conntrack_map, which belongs to the core filter.
|
||||
*
|
||||
* ── HOW TO ATTACH A HOOK ──
|
||||
*
|
||||
* 1. Create hooks/<proto>_hook.h containing ONLY your own includes
|
||||
* (common.h etc.) and a definition of proto_hook(). Do NOT include
|
||||
* hook_api.h from the hook header — the core includes it for you:
|
||||
*
|
||||
* // hooks/myproto_hook.h
|
||||
* #include "../core/common.h"
|
||||
* static __always_inline enum xdp_action
|
||||
* proto_hook(struct cursor *c, struct pkt_meta *m)
|
||||
* {
|
||||
* ... parse m, advance c ...
|
||||
* return XDP_PASS; // or XDP_DROP
|
||||
* }
|
||||
*
|
||||
* 2. Build the object with the macro pointing at your header:
|
||||
*
|
||||
* clang -O2 -g -target bpf \
|
||||
* -DRAMPART_PROTO_HOOK='"../hooks/myproto_hook.h"' \
|
||||
* -c xdp/core/universal_filter.c -o universal_filter.o
|
||||
*
|
||||
* Without RAMPART_PROTO_HOOK the default no-op stub below is used
|
||||
* and every established connection passes.
|
||||
*/
|
||||
|
||||
#include <linux/bpf.h>
|
||||
#include "../core/common.h"
|
||||
|
||||
// ── Bounds-checked cursor over the payload window ──
|
||||
struct cursor {
|
||||
__u8 *pos; // next unparsed byte
|
||||
__u8 *end; // one past last valid byte (== data_end)
|
||||
};
|
||||
|
||||
// Take n bytes from the cursor; returns pointer or NULL on overrun.
|
||||
static __always_inline __u8 *cursor_take(struct cursor *c, __u32 n)
|
||||
{
|
||||
if (c->pos + n > c->end)
|
||||
return NULL;
|
||||
barrier_var(c->pos);
|
||||
__u8 *p = c->pos;
|
||||
c->pos += n;
|
||||
return p;
|
||||
}
|
||||
|
||||
// ── Read-only packet metadata handed to the hook ──
|
||||
struct pkt_meta {
|
||||
__u32 src_ip; // IPv4 addr, or low 32 bits of IPv4-mapped IPv6
|
||||
__u32 dst_ip; // same convention as src_ip
|
||||
__u16 src_port; // host byte order
|
||||
__u16 dst_port; // host byte order
|
||||
__u16 payload_len; // TCP payload bytes in this segment
|
||||
__u8 tcp_flags; // full 13th-byte flags field
|
||||
__u8 is_ipv6; // 1 if L3 is IPv4-mapped IPv6
|
||||
};
|
||||
|
||||
#ifdef RAMPART_PROTO_HOOK
|
||||
#include RAMPART_PROTO_HOOK
|
||||
#else
|
||||
/* Default stub: no deep inspection, everything established passes. */
|
||||
static __always_inline enum xdp_action
|
||||
proto_hook(struct cursor *c, struct pkt_meta *m)
|
||||
{
|
||||
return XDP_PASS;
|
||||
}
|
||||
#endif /* RAMPART_PROTO_HOOK */
|
||||
|
||||
#endif /* RAMPART_HOOK_API_H */
|
||||
183
xdp/protocol.h
183
xdp/protocol.h
|
|
@ -1,183 +0,0 @@
|
|||
#ifndef RAMPART_PROTOCOL_H
|
||||
#define RAMPART_PROTOCOL_H
|
||||
|
||||
#include "common.h"
|
||||
#include "varint.h"
|
||||
|
||||
// ── Handshake inspector ──
|
||||
// Returns: pseudo-state for next action, or 0 on failure
|
||||
// DIRECT_READ_LOGIN — handshake + login in same segment
|
||||
// DIRECT_READ_STATUS — handshake + status in same segment
|
||||
// AWAIT_LOGIN — handshake only, wait for login
|
||||
// AWAIT_STATUS — handshake only, wait for status req
|
||||
// RECEIVED_LEGACY_PING — 0xFE legacy ping
|
||||
// 0 — parse error
|
||||
|
||||
#define DIRECT_READ_LOGIN 100
|
||||
#define DIRECT_READ_STATUS 101
|
||||
#define RECEIVED_LEGACY_PING 102
|
||||
|
||||
static __always_inline __s32 inspect_handshake(
|
||||
__u8 **cursor, __u8 *payload_end, __s32 *protocol, void *data_end)
|
||||
{
|
||||
__u8 *ptr = *cursor;
|
||||
|
||||
// Legacy ping check (0xFE)
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 1, payload_end, data_end);
|
||||
if (ptr[0] == (__u8)0xFE) {
|
||||
return RECEIVED_LEGACY_PING;
|
||||
}
|
||||
|
||||
// Read total packet length
|
||||
struct varint_value pkt_len = read_varint(ptr, payload_end, data_end);
|
||||
if (pkt_len.bytes == 0) goto error;
|
||||
ptr += pkt_len.bytes;
|
||||
|
||||
// Packet ID must be 0 (Handshake)
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 1, payload_end, data_end);
|
||||
struct varint_value pkid = read_varint(ptr, payload_end, data_end);
|
||||
if (pkid.bytes == 0 || pkid.value != 0) goto error;
|
||||
ptr += pkid.bytes;
|
||||
|
||||
// Protocol version
|
||||
struct varint_value pv = read_varint(ptr, payload_end, data_end);
|
||||
if (pv.bytes == 0 || pv.value < 0) goto error;
|
||||
*protocol = pv.value;
|
||||
ptr += pv.bytes;
|
||||
|
||||
// Server address (varint-length-prefixed string, max 765 bytes)
|
||||
struct varint_value addr_len = read_varint(ptr, payload_end, data_end);
|
||||
if (addr_len.bytes == 0 || addr_len.value < 0 || addr_len.value > 765)
|
||||
goto error;
|
||||
ptr += addr_len.bytes;
|
||||
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, addr_len.value, payload_end, data_end);
|
||||
ptr += addr_len.value;
|
||||
|
||||
// Server port (u16)
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 2, payload_end, data_end);
|
||||
// __u16 port = (ptr[0] << 8) | ptr[1]; — skip, not used
|
||||
ptr += 2;
|
||||
|
||||
// Next state (1=status, 2=login, 3=transfer since 1.20.5)
|
||||
struct varint_value ns = read_varint(ptr, payload_end, data_end);
|
||||
if (ns.bytes == 0) goto error;
|
||||
if (ns.value != 1 && ns.value != 2 && ns.value != 3) goto error;
|
||||
ptr += ns.bytes;
|
||||
|
||||
// Verify declared length matches consumed
|
||||
__u32 consumed = (__u32)(ptr - *cursor);
|
||||
__u32 declared_len = (__u32)(pkt_len.value + pkt_len.bytes);
|
||||
if (consumed > declared_len) goto error;
|
||||
|
||||
*cursor = ptr;
|
||||
|
||||
// Check if more data follows in same segment
|
||||
if (consumed < declared_len + 2) {
|
||||
// Handshake only — return expected next state
|
||||
return (ns.value == 1) ? 103 /* AWAIT_STATUS */ : 104 /* AWAIT_LOGIN */;
|
||||
}
|
||||
|
||||
// More data present — return direct-read state
|
||||
return (ns.value == 1) ? DIRECT_READ_STATUS : DIRECT_READ_LOGIN;
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── LoginStart inspector ──
|
||||
static __always_inline __u8 inspect_login_packet(
|
||||
__u8 *ptr, __u8 *payload_end, __s32 protocol, void *data_end)
|
||||
{
|
||||
// Packet length
|
||||
struct varint_value pkt_len = read_varint(ptr, payload_end, data_end);
|
||||
if (pkt_len.bytes == 0) goto error;
|
||||
ptr += pkt_len.bytes;
|
||||
|
||||
// Packet ID must be 0 (LoginStart)
|
||||
struct varint_value pkid = read_varint(ptr, payload_end, data_end);
|
||||
if (pkid.bytes == 0 || pkid.value != 0) goto error;
|
||||
ptr += pkid.bytes;
|
||||
|
||||
// Username (varint-length-prefixed string)
|
||||
struct varint_value name_len = read_varint(ptr, payload_end, data_end);
|
||||
if (name_len.bytes == 0 || name_len.value <= 0) goto error;
|
||||
ptr += name_len.bytes;
|
||||
|
||||
__s32 max_name = (protocol >= 764) ? 16 : 48; // 1.20.2+ uses 16
|
||||
if (name_len.value > max_name || name_len.value > 48) goto error;
|
||||
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, name_len.value, payload_end, data_end);
|
||||
// Skip username bytes
|
||||
ptr += name_len.value;
|
||||
|
||||
// For 1.19.1+ (protocol >= 760): optional UUID
|
||||
if (protocol >= 760) {
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 1, payload_end, data_end);
|
||||
__u8 has_uuid = ptr[0];
|
||||
ptr += 1;
|
||||
if (has_uuid) {
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 16, payload_end, data_end);
|
||||
ptr += 16; // skip UUID
|
||||
}
|
||||
}
|
||||
|
||||
// For 1.19-1.19.2 (759-760): optional public key
|
||||
if (protocol >= 759 && protocol < 761) {
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 1, payload_end, data_end);
|
||||
__u8 has_key = ptr[0];
|
||||
ptr += 1;
|
||||
if (has_key) {
|
||||
// Expiry (long)
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 8, payload_end, data_end);
|
||||
ptr += 8;
|
||||
// Key length (varint)
|
||||
struct varint_value key_len = read_varint(ptr, payload_end, data_end);
|
||||
if (key_len.bytes == 0) goto error;
|
||||
ptr += key_len.bytes;
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, key_len.value, payload_end, data_end);
|
||||
ptr += key_len.value;
|
||||
// Signature length (varint)
|
||||
struct varint_value sig_len = read_varint(ptr, payload_end, data_end);
|
||||
if (sig_len.bytes == 0) goto error;
|
||||
ptr += sig_len.bytes;
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, sig_len.value, payload_end, data_end);
|
||||
ptr += sig_len.value;
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // success
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Status request inspector ──
|
||||
// Must be: [len=0x01][id=0x00]
|
||||
static __always_inline __u8 inspect_status_request(
|
||||
__u8 *ptr, __u8 *payload_end, void *data_end)
|
||||
{
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 2, payload_end, data_end);
|
||||
if (ptr[0] != 0x01 || ptr[1] != 0x00)
|
||||
goto error;
|
||||
return 1;
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Ping request inspector ──
|
||||
// Must be: [len=0x09][id=0x01][8 byte timestamp/long]
|
||||
static __always_inline __u8 inspect_ping_request(
|
||||
__u8 *ptr, __u8 *payload_end, void *data_end)
|
||||
{
|
||||
CHECK_BOUNDS_OR_RETURN(ptr, 10, payload_end, data_end);
|
||||
if (ptr[0] != 0x09 || ptr[1] != 0x01)
|
||||
goto error;
|
||||
return 1;
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* RAMPART_PROTOCOL_H */
|
||||
25
xdp/stats.h
25
xdp/stats.h
|
|
@ -1,25 +0,0 @@
|
|||
#ifndef RAMPART_STATS_H
|
||||
#define RAMPART_STATS_H
|
||||
|
||||
#include "common.h"
|
||||
#include "maps.h"
|
||||
|
||||
// ── Per-CPU stat increment ──
|
||||
static __always_inline void inc_stat(__u32 idx)
|
||||
{
|
||||
__u64 *val = bpf_map_lookup_elem(&stats_map, &idx);
|
||||
if (val)
|
||||
__sync_fetch_and_add(val, 1);
|
||||
}
|
||||
|
||||
// ── Convenience wrappers ──
|
||||
static __always_inline void inc_total(void) { inc_stat(STAT_TOTAL); }
|
||||
static __always_inline void inc_tcp_mc(void) { inc_stat(STAT_TCP_MC); }
|
||||
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_pass(void) { inc_stat(STAT_PASS); }
|
||||
static __always_inline void inc_drop(void) { inc_stat(STAT_DROP); }
|
||||
static __always_inline void inc_verified(void){ inc_stat(STAT_VERIFIED); }
|
||||
|
||||
#endif /* RAMPART_STATS_H */
|
||||
63
xdp/varint.h
63
xdp/varint.h
|
|
@ -1,63 +0,0 @@
|
|||
#ifndef RAMPART_VARINT_H
|
||||
#define RAMPART_VARINT_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// ── VarInt reader with dual-bounds check ──
|
||||
// Returns {value, bytes_consumed} on success, goto error on failure
|
||||
// Fixed: no sign extension UB (shift in u32, cast to s32)
|
||||
// Fixed: max 5 bytes per Minecraft protocol spec
|
||||
|
||||
#define VARINT_BYTE(ptr, pend, dend, max, idx, shift, result) \
|
||||
do { \
|
||||
if ((max) < (idx)) \
|
||||
goto error; \
|
||||
if ((void *)(ptr) + 1 > (void *)(dend)) \
|
||||
goto error; \
|
||||
if ((void *)(ptr) + 1 > (void *)(pend)) \
|
||||
goto error; \
|
||||
__u8 _b = *(ptr)++; \
|
||||
(result) |= ((__u32)(_b & 0x7F) << (shift)); \
|
||||
if (!(_b & 0x80)) \
|
||||
return varint((__s32)(result), (idx)); \
|
||||
} while (0)
|
||||
|
||||
struct varint_value {
|
||||
__s32 value;
|
||||
__u8 bytes;
|
||||
};
|
||||
|
||||
static __always_inline struct varint_value varint(__s32 value, __u8 bytes)
|
||||
{
|
||||
struct varint_value v = { .value = value, .bytes = bytes };
|
||||
return v;
|
||||
}
|
||||
|
||||
static __always_inline struct varint_value read_varint_sized(
|
||||
__u8 *ptr, __u8 *pend, __u8 max, void *dend)
|
||||
{
|
||||
__u32 result = 0;
|
||||
|
||||
VARINT_BYTE(ptr, pend, dend, max, 1, 0, result);
|
||||
VARINT_BYTE(ptr, pend, dend, max, 2, 7, result);
|
||||
VARINT_BYTE(ptr, pend, dend, max, 3, 14, result);
|
||||
VARINT_BYTE(ptr, pend, dend, max, 4, 21, result);
|
||||
if (max < 5) goto error;
|
||||
if ((void *)(ptr) + 1 > (void *)(dend)) goto error;
|
||||
if ((void *)(ptr) + 1 > (void *)(pend)) goto error;
|
||||
__u8 b5 = *(ptr)++;
|
||||
result |= ((__u32)(b5 & 0x7F) << 28);
|
||||
result &= 0x7FFFFFFF;
|
||||
return varint((__s32)result, 5);
|
||||
|
||||
error:
|
||||
return varint(0, 0);
|
||||
}
|
||||
|
||||
// Convenience: read varint with max=5 (full Minecraft varint)
|
||||
static __always_inline struct varint_value read_varint(__u8 *ptr, __u8 *pend, void *dend)
|
||||
{
|
||||
return read_varint_sized(ptr, pend, 5, dend);
|
||||
}
|
||||
|
||||
#endif /* RAMPART_VARINT_H */
|
||||
440
xdp/xdp_filter.c
440
xdp/xdp_filter.c
|
|
@ -1,440 +0,0 @@
|
|||
// ── Rampart XDP/eBPF Filter ──
|
||||
// Stateful TCP connection inspection for Minecraft Java Edition.
|
||||
// Drops malicious traffic at NIC driver level, before kernel TCP stack.
|
||||
//
|
||||
// Based on research of existing XDP filters for Minecraft.
|
||||
// Key fixes vs other implementations:
|
||||
// 1. No pure ACK drop (prevents TCP handshake deadlock)
|
||||
// 2. LRU maps for both conntrack and player entries
|
||||
// 3. bpf_timer idle cleanup on conntrack entries (not just player)
|
||||
// 4. RST/FIN removes conntrack entry (no stale state)
|
||||
// 5. IPv6 support alongside IPv4
|
||||
// 6. IP/CIDR whitelist (LPM_TRIE)
|
||||
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/ip.h>
|
||||
#include <linux/ipv6.h>
|
||||
#include <linux/tcp.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "maps.h"
|
||||
#include "config.h"
|
||||
#include "varint.h"
|
||||
#include "protocol.h"
|
||||
#include "stats.h"
|
||||
|
||||
char __license[] SEC("license") = "GPL";
|
||||
|
||||
// Timer-based cleanup not needed: all maps are LRU and self-evicting.
|
||||
// Stale entries in conntrack_map/player_connection_map are evicted
|
||||
// automatically by the kernel when the maps fill up.
|
||||
|
||||
// ── TCP flag check (drop malicious combos) ──
|
||||
// Returns 1 if packet should be dropped
|
||||
static __always_inline __u8 detect_tcp_bypass(struct tcphdr *tcp)
|
||||
{
|
||||
__u8 flags = *((__u8 *)tcp + 13);
|
||||
|
||||
// No flags at all — bogus packet
|
||||
if ((flags & 0x3F) == 0)
|
||||
return 1;
|
||||
|
||||
// SYN+FIN or SYN+RST — always forged
|
||||
if (tcp->syn) {
|
||||
if (tcp->fin || tcp->rst)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SYN+ACK from client side — only servers send this
|
||||
if (tcp->syn && tcp->ack)
|
||||
return 1;
|
||||
|
||||
// URG flag — unused in Minecraft protocol
|
||||
if (tcp->urg)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Switch connection to verified state ──
|
||||
static __always_inline __u8 switch_to_verified(struct flow_key *flow)
|
||||
{
|
||||
struct player_entry entry = {};
|
||||
entry.protocol = 0;
|
||||
|
||||
if (bpf_map_update_elem(&player_connection_map, flow, &entry, BPF_NOEXIST)) {
|
||||
// Map is full — LRU will evict, but we still drop this one
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove from conntrack
|
||||
bpf_map_delete_elem(&conntrack_map, flow);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════
|
||||
// Main XDP entry point
|
||||
// ════════════════════════════════════════════════════
|
||||
SEC("xdp")
|
||||
int rampart_xdp_filter(struct xdp_md *ctx)
|
||||
{
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
void *data = (void *)(long)ctx->data;
|
||||
|
||||
inc_total();
|
||||
|
||||
// ── Parse Ethernet header ──
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
// Non-IP traffic: pass (ARP, etc.)
|
||||
if (eth->h_proto != bpf_htons(ETH_P_IP) && eth->h_proto != bpf_htons(ETH_P_IPV6))
|
||||
return XDP_PASS;
|
||||
|
||||
// ── Parse IP header ──
|
||||
__u32 src_ip = 0;
|
||||
__u8 is_ipv6 = 0;
|
||||
|
||||
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
|
||||
// IPv4
|
||||
struct iphdr *ip = (void *)(eth + 1);
|
||||
if ((void *)(ip + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
if (ip->ihl < 5)
|
||||
return XDP_DROP;
|
||||
|
||||
// Non-TCP: pass
|
||||
if (ip->protocol != IPPROTO_TCP)
|
||||
return XDP_PASS;
|
||||
|
||||
// Non-sequential fragment: pass (can't inspect ports safely)
|
||||
if (ip->frag_off & bpf_htons(IP_OFFSET))
|
||||
return XDP_PASS;
|
||||
|
||||
// First fragment with MF: drop (can't reassemble)
|
||||
if (ip->frag_off & bpf_htons(IP_MF))
|
||||
return XDP_DROP;
|
||||
|
||||
src_ip = ip->saddr;
|
||||
} else {
|
||||
// IPv6
|
||||
struct ipv6hdr *ip6 = (void *)(eth + 1);
|
||||
if ((void *)(ip6 + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
// Non-TCP: pass
|
||||
if (ip6->nexthdr != IPPROTO_TCP)
|
||||
return XDP_PASS;
|
||||
|
||||
// Use IPv4-mapped IPv6 address for flow key (::ffff:0:0/96)
|
||||
if (ip6->daddr.in6_u.u6_addr32[0] != 0 ||
|
||||
ip6->daddr.in6_u.u6_addr32[1] != 0 ||
|
||||
ip6->daddr.in6_u.u6_addr32[2] != bpf_htonl(0xFFFF)) {
|
||||
// Non-mapped IPv6 — pass for now (not supported)
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
src_ip = ip6->daddr.in6_u.u6_addr32[3];
|
||||
is_ipv6 = 1;
|
||||
}
|
||||
|
||||
// ── Parse TCP header ──
|
||||
struct tcphdr *tcp;
|
||||
__u8 ip_hdr_len;
|
||||
|
||||
if (is_ipv6) {
|
||||
struct ipv6hdr *ip6 = (void *)(eth + 1);
|
||||
tcp = (void *)(ip6 + 1);
|
||||
ip_hdr_len = sizeof(struct ipv6hdr);
|
||||
} else {
|
||||
struct iphdr *ip = (void *)(eth + 1);
|
||||
tcp = (void *)ip + (ip->ihl * 4);
|
||||
ip_hdr_len = ip->ihl * 4;
|
||||
}
|
||||
|
||||
if ((void *)(tcp + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
// TCP header length check
|
||||
if (tcp->doff < 5)
|
||||
return XDP_DROP;
|
||||
__u8 tcp_hdr_len = tcp->doff * 4;
|
||||
if ((void *)data + sizeof(struct ethhdr) + ip_hdr_len + tcp_hdr_len > data_end)
|
||||
return XDP_DROP;
|
||||
|
||||
// ── Port check ──
|
||||
// Only filter Minecraft ports
|
||||
__u16 dst_port = bpf_ntohs(tcp->dest);
|
||||
if (dst_port < G_START_PORT || dst_port > G_END_PORT)
|
||||
return XDP_PASS;
|
||||
|
||||
inc_tcp_mc();
|
||||
|
||||
// ── Whitelist check (CIDR) ──
|
||||
struct lpm_key wl_key = { .prefixlen = 32, .ip = src_ip };
|
||||
if (bpf_map_lookup_elem(&whitelist_map, &wl_key)) {
|
||||
inc_whitelist();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// ── Malicious TCP flags ──
|
||||
if (detect_tcp_bypass(tcp)) {
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_DEATH_CODE, src_ip, 0);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Compute payload pointers
|
||||
__u8 *tcp_payload = (__u8 *)tcp + tcp_hdr_len;
|
||||
__u8 *tcp_payload_end = (__u8 *)data_end;
|
||||
__s32 tcp_payload_len = (__s32)(tcp_payload_end - tcp_payload);
|
||||
if (tcp_payload_len < 0)
|
||||
tcp_payload_len = 0;
|
||||
|
||||
// ── Build flow key ──
|
||||
struct flow_key flow = {
|
||||
.src_ip = src_ip,
|
||||
.dst_ip = is_ipv6 ? 0 : ((struct iphdr *)(eth + 1))->daddr,
|
||||
.src_port = tcp->source,
|
||||
.dst_port = tcp->dest,
|
||||
};
|
||||
|
||||
// ── Verified player fast path ──
|
||||
struct player_entry *player = bpf_map_lookup_elem(&player_connection_map, &flow);
|
||||
if (player) {
|
||||
player->packets++;
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// ── SYN handling (new connection) ──
|
||||
if (tcp->syn && !tcp->ack) {
|
||||
// SYN throttle
|
||||
if (G_FEATURE_SYN_THROTTLE) {
|
||||
struct throttle_entry *th = bpf_map_lookup_elem(&connection_throttle, &src_ip);
|
||||
__u64 now = bpf_ktime_get_ns();
|
||||
|
||||
if (th) {
|
||||
if (now - th->window_start < G_SYN_WINDOW_NS) {
|
||||
if (th->hits >= G_SYN_HIT_COUNT) {
|
||||
// Ban this IP
|
||||
struct lpm_key ban_key = { .prefixlen = 32, .ip = src_ip };
|
||||
__u64 ban_until = now + G_SYN_BAN_DURATION_NS;
|
||||
bpf_map_update_elem(&blacklist_map, &ban_key, &ban_until, BPF_ANY);
|
||||
inc_syn_throttle();
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_RATE_LIMIT, src_ip, th->hits);
|
||||
return XDP_DROP;
|
||||
}
|
||||
__sync_fetch_and_add(&th->hits, 1);
|
||||
} else {
|
||||
th->window_start = now;
|
||||
th->hits = 1;
|
||||
}
|
||||
} else {
|
||||
struct throttle_entry new_th = { .window_start = now, .hits = 1 };
|
||||
bpf_map_update_elem(&connection_throttle, &src_ip, &new_th, BPF_ANY);
|
||||
}
|
||||
}
|
||||
|
||||
// Create conntrack entry for new connection
|
||||
struct conntrack_entry ce = {};
|
||||
ce.state = STATE_AWAIT_ACK;
|
||||
ce.expected_seq = bpf_ntohl(tcp->seq) + 1; // expect SYN-ACK seq
|
||||
ce.src_ip = src_ip;
|
||||
ce.src_port = tcp->source;
|
||||
|
||||
if (bpf_map_update_elem(&conntrack_map, &flow, &ce, BPF_ANY)) {
|
||||
// Map full — should not happen with LRU
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
inc_pass();
|
||||
return XDP_PASS; // Let SYN through
|
||||
}
|
||||
|
||||
// ── Connection tracking lookup ──
|
||||
struct conntrack_entry *conn = bpf_map_lookup_elem(&conntrack_map, &flow);
|
||||
if (!conn) {
|
||||
// Unknown connection — drop
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// ── Sequence number tracking ──
|
||||
__u32 seq = bpf_ntohl(tcp->seq);
|
||||
if (conn->state != STATE_AWAIT_ACK && tcp_payload_len > 0) {
|
||||
if (seq != conn->expected_seq) {
|
||||
conn->fails++;
|
||||
if (conn->fails >= G_MAX_OUT_OF_ORDER) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_CONN_DROP, src_ip, conn->fails);
|
||||
return XDP_DROP;
|
||||
}
|
||||
// Allow retransmission (don't update expected_seq)
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State machine ──
|
||||
__u32 state = conn->state;
|
||||
|
||||
// Handle RST/FIN — clean up conntrack entry
|
||||
if (tcp->rst || tcp->fin) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
if (state == STATE_AWAIT_ACK) {
|
||||
// Expecting ACK that completes TCP 3-way handshake
|
||||
if (!tcp->ack || seq != conn->expected_seq) {
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// ═══ FIX vs other implementations ═══
|
||||
// Do NOT drop pure ACK here. Other XDP filters drop the pure ACK
|
||||
// expecting the data packet to serve as ACK, which causes ~1-7s
|
||||
// TCP handshake deadlock. We pass the ACK through.
|
||||
conn->state = STATE_AWAIT_MC_HANDSHAKE;
|
||||
conn->expected_seq = seq + tcp_payload_len;
|
||||
|
||||
// If this is a pure ACK (no data), pass it through
|
||||
if (tcp_payload_len == 0) {
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
// Fall through to handshake inspection
|
||||
}
|
||||
else if (state == STATE_AWAIT_MC_HANDSHAKE) {
|
||||
// Pure ACK without data — pass through (keep-alive, etc.)
|
||||
if (tcp_payload_len == 0) {
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Inspect handshake packet
|
||||
__u8 *cursor = tcp_payload;
|
||||
__s32 protocol = 0;
|
||||
|
||||
__s32 result = inspect_handshake(&cursor, tcp_payload_end, &protocol, data_end);
|
||||
|
||||
// Update expected sequence
|
||||
__u32 data_consumed = (__u32)(cursor - tcp_payload);
|
||||
conn->expected_seq += data_consumed;
|
||||
|
||||
if (result == 0) {
|
||||
// Malformed handshake — ban
|
||||
struct lpm_key ban_key = { .prefixlen = 32, .ip = src_ip };
|
||||
__u64 now = bpf_ktime_get_ns();
|
||||
__u64 ban_until = now + G_BAN_DURATION_NS;
|
||||
bpf_map_update_elem(&blacklist_map, &ban_key, &ban_until, BPF_ANY);
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_BAN, src_ip, 1);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
if (result == RECEIVED_LEGACY_PING) {
|
||||
// Legacy ping (pre-1.7) — drop connection
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
if (result == DIRECT_READ_LOGIN) {
|
||||
// Handshake + login in same segment
|
||||
__u8 login_ok = inspect_login_packet(cursor, tcp_payload_end, protocol, data_end);
|
||||
__u32 login_consumed = (__u32)(tcp_payload_end - cursor);
|
||||
if (login_consumed < (__u32)(tcp_payload_end - cursor))
|
||||
conn->expected_seq += login_consumed;
|
||||
|
||||
if (!login_ok) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Login passed — switch to verified
|
||||
if (switch_to_verified(&flow)) {
|
||||
conn->state = STATE_VERIFIED;
|
||||
inc_verified();
|
||||
if (G_FEATURE_EVENTS)
|
||||
push_event(EVENT_VERIFIED, src_ip, protocol);
|
||||
return XDP_PASS;
|
||||
}
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
if (result == DIRECT_READ_STATUS) {
|
||||
// Handshake + status request in same segment
|
||||
// Forge-style — pass through
|
||||
conn->state = STATE_PING_COMPLETE;
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Normal: handshake only, wait for login
|
||||
conn->state = STATE_AWAIT_LOGIN;
|
||||
conn->expected_seq = seq + data_consumed;
|
||||
conn->protocol = (__u32)protocol;
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
else if (state == STATE_AWAIT_LOGIN) {
|
||||
if (tcp_payload_len == 0) {
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
__u8 login_ok = inspect_login_packet(tcp_payload, tcp_payload_end,
|
||||
(__s32)conn->protocol, data_end);
|
||||
|
||||
if (!login_ok) {
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Login passed — move to verified
|
||||
if (switch_to_verified(&flow)) {
|
||||
conn->state = STATE_VERIFIED;
|
||||
inc_verified();
|
||||
return XDP_PASS;
|
||||
}
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
else if (state == STATE_VERIFIED) {
|
||||
// Should not happen — verified connections use fast path
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
}
|
||||
else if (state == STATE_PING_COMPLETE) {
|
||||
// Ping completed — drop connection
|
||||
bpf_map_delete_elem(&conntrack_map, &flow);
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// Unknown state — pass
|
||||
inc_pass();
|
||||
return XDP_PASS;
|
||||
|
||||
error:
|
||||
inc_drop();
|
||||
return XDP_DROP;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue