Add CDN filter and fix xdp-cdn-update bugs
- Add xdp_cdn_filter BPF program (priority 5) to allow only CDN/whitelist on port 80/443 - Fix \r carriage return bug preventing BunnyCDN IPv4 loading (594 IPs were silently failing) - Fix BPF map flush code to handle list-type keys from bpftool JSON output - Fix per-cpu stats parsing to use formatted values from bpftool - Replace in-loop counter with post-load BPF map verification for accurate counts - Remove xdp_cdn_load.py (consolidated into xdp-cdn-update) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
228
bpf/xdp_cdn_filter.c
Normal file
228
bpf/xdp_cdn_filter.c
Normal file
@@ -0,0 +1,228 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// XDP CDN Filter - Port 80/443 트래픽을 CDN IP + 화이트리스트 IP만 허용
|
||||
// Part of xdp-defense: chained via libxdp dispatcher (priority 5)
|
||||
#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 <linux/in.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include <xdp/xdp_helpers.h>
|
||||
|
||||
// LPM trie keys (must match xdp_blocker.c exactly)
|
||||
struct ipv4_lpm_key {
|
||||
__u32 prefixlen;
|
||||
__u32 addr;
|
||||
};
|
||||
|
||||
struct ipv6_lpm_key {
|
||||
__u32 prefixlen;
|
||||
__u8 addr[16];
|
||||
};
|
||||
|
||||
// VLAN header (802.1Q)
|
||||
struct vlan_hdr {
|
||||
__be16 h_vlan_TCI;
|
||||
__be16 h_vlan_encapsulated_proto;
|
||||
};
|
||||
|
||||
// Shared whitelist maps (pinned by xdp_blocker, reused here)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv4_lpm_key);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(pinning, LIBBPF_PIN_BY_NAME);
|
||||
} whitelist_v4 SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv6_lpm_key);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
__uint(pinning, LIBBPF_PIN_BY_NAME);
|
||||
} whitelist_v6 SEC(".maps");
|
||||
|
||||
// CDN IPv4 allowlist (BunnyNet + Cloudflare)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv4_lpm_key);
|
||||
__type(value, __u8);
|
||||
__uint(max_entries, 4096);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
} cdn_allow_v4 SEC(".maps");
|
||||
|
||||
// CDN IPv6 allowlist (BunnyNet + Cloudflare)
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||||
__type(key, struct ipv6_lpm_key);
|
||||
__type(value, __u8);
|
||||
__uint(max_entries, 1024);
|
||||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||||
} cdn_allow_v6 SEC(".maps");
|
||||
|
||||
// Per-CPU stats: 0=passed(cdn), 1=passed(whitelist), 2=passed(non-web), 3=dropped
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
|
||||
__type(key, __u32);
|
||||
__type(value, __u64);
|
||||
__uint(max_entries, 4);
|
||||
} cdn_stats SEC(".maps");
|
||||
|
||||
static __always_inline void inc_stat(__u32 idx) {
|
||||
__u64 *val = bpf_map_lookup_elem(&cdn_stats, &idx);
|
||||
if (val)
|
||||
(*val)++;
|
||||
}
|
||||
|
||||
// Extract dest port from transport header. Returns 0 if not TCP/UDP.
|
||||
static __always_inline __u16 get_dport(void *transport, void *data_end, __u8 proto) {
|
||||
if (proto == IPPROTO_TCP) {
|
||||
struct tcphdr *tcp = transport;
|
||||
if ((void *)(tcp + 1) > data_end)
|
||||
return 0;
|
||||
return bpf_ntohs(tcp->dest);
|
||||
}
|
||||
if (proto == IPPROTO_UDP) {
|
||||
struct udphdr *udp = transport;
|
||||
if ((void *)(udp + 1) > data_end)
|
||||
return 0;
|
||||
return bpf_ntohs(udp->dest);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("xdp")
|
||||
int xdp_cdn_filter(struct xdp_md *ctx) {
|
||||
void *data = (void *)(long)ctx->data;
|
||||
void *data_end = (void *)(long)ctx->data_end;
|
||||
|
||||
struct ethhdr *eth = data;
|
||||
if ((void *)(eth + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
__u16 eth_proto = bpf_ntohs(eth->h_proto);
|
||||
void *l3_hdr = (void *)(eth + 1);
|
||||
|
||||
// Handle VLAN tags
|
||||
if (eth_proto == ETH_P_8021Q || eth_proto == ETH_P_8021AD) {
|
||||
struct vlan_hdr *vhdr = l3_hdr;
|
||||
if ((void *)(vhdr + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
eth_proto = bpf_ntohs(vhdr->h_vlan_encapsulated_proto);
|
||||
l3_hdr = (void *)(vhdr + 1);
|
||||
if (eth_proto == ETH_P_8021Q || eth_proto == ETH_P_8021AD) {
|
||||
vhdr = l3_hdr;
|
||||
if ((void *)(vhdr + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
eth_proto = bpf_ntohs(vhdr->h_vlan_encapsulated_proto);
|
||||
l3_hdr = (void *)(vhdr + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// === IPv4 ===
|
||||
if (eth_proto == ETH_P_IP) {
|
||||
struct iphdr *iph = l3_hdr;
|
||||
if ((void *)(iph + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
if (iph->ihl < 5)
|
||||
return XDP_PASS;
|
||||
|
||||
__u8 proto = iph->protocol;
|
||||
void *transport = (void *)iph + (iph->ihl * 4);
|
||||
__u16 dport = get_dport(transport, data_end, proto);
|
||||
|
||||
// Not port 80/443 → pass (don't interfere with SSH, Tailscale, etc.)
|
||||
if (dport != 80 && dport != 443) {
|
||||
inc_stat(2);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
struct ipv4_lpm_key key = { .prefixlen = 32, .addr = iph->saddr };
|
||||
|
||||
// Check shared whitelist (internal/Tailscale IPs)
|
||||
if (bpf_map_lookup_elem(&whitelist_v4, &key)) {
|
||||
inc_stat(1);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Check CDN allowlist
|
||||
if (bpf_map_lookup_elem(&cdn_allow_v4, &key)) {
|
||||
inc_stat(0);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
// Not CDN, not whitelisted → DROP
|
||||
inc_stat(3);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
// === IPv6 ===
|
||||
if (eth_proto == ETH_P_IPV6) {
|
||||
struct ipv6hdr *ip6h = l3_hdr;
|
||||
if ((void *)(ip6h + 1) > data_end)
|
||||
return XDP_PASS;
|
||||
|
||||
__u8 proto = ip6h->nexthdr;
|
||||
void *transport = (void *)(ip6h + 1);
|
||||
|
||||
// Skip extension headers (up to 4)
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (proto != IPPROTO_HOPOPTS && proto != IPPROTO_ROUTING &&
|
||||
proto != IPPROTO_DSTOPTS && proto != IPPROTO_FRAGMENT)
|
||||
break;
|
||||
if (proto == IPPROTO_FRAGMENT) {
|
||||
if (transport + 8 > data_end) break;
|
||||
proto = *(__u8 *)transport;
|
||||
transport += 8;
|
||||
} else {
|
||||
if (transport + 2 > data_end) break;
|
||||
__u8 ext_len = *((__u8 *)transport + 1);
|
||||
__u32 hdr_len = (((__u32)ext_len) + 1) * 8;
|
||||
if (transport + hdr_len > data_end) break;
|
||||
proto = *(__u8 *)transport;
|
||||
transport += hdr_len;
|
||||
}
|
||||
}
|
||||
|
||||
__u16 dport = get_dport(transport, data_end, proto);
|
||||
|
||||
if (dport != 80 && dport != 443) {
|
||||
inc_stat(2);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
struct ipv6_lpm_key key = { .prefixlen = 128 };
|
||||
__builtin_memcpy(key.addr, &ip6h->saddr, 16);
|
||||
|
||||
if (bpf_map_lookup_elem(&whitelist_v6, &key)) {
|
||||
inc_stat(1);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
if (bpf_map_lookup_elem(&cdn_allow_v6, &key)) {
|
||||
inc_stat(0);
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
inc_stat(3);
|
||||
return XDP_DROP;
|
||||
}
|
||||
|
||||
return XDP_PASS; // ARP etc.
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
// libxdp dispatcher: priority 5, chain on XDP_PASS
|
||||
struct {
|
||||
__uint(priority, 5);
|
||||
__uint(XDP_PASS, 1);
|
||||
} XDP_RUN_CONFIG(xdp_cdn_filter);
|
||||
Reference in New Issue
Block a user