Tutorial: Mitigating DDoS Attacks on a VPS with eBPF/XDP on Ubuntu 22.04 — A Practical Walkthrough

Redes & Segurança · By · Published on · 9 min read

Orange lines and dots connected to form an abstract network over a dark background
Abstract illustration representing network traffic and connections Photo: U.Lucas Dubé-Cantin / Pexels

Step-by-step tutorial to mitigate DDoS attacks on a VPS using eBPF/XDP on Ubuntu 22.04. You'll compile a simple XDP program, attach it to your network interface, validate counters and block IPs with bpftool. This low-latency approach drops malicious traffic before it reaches the network stack. Test carefully and keep a recovery path available.

Prerequisites, risks and goal

We'll filter packets in the driver with XDP (Express Data Path) using eBPF. This requires root access and can disrupt connectivity if applied incorrectly. Test in an isolated environment or ensure you have out-of-band console access.

The goal is to block obvious sources (spoofed and reserved IPs), discard IP fragments and—optionally—drop UDP to HTTP ports while under attack. Integrate this with other hardening practices.

Before starting, make sure the basics are in place and install the required tools.

  • Ubuntu Server 22.04 with the default kernel.
  • Root or sudo privileges.
  • Clang/LLVM, kernel headers, bpftool, iproute2 and xdp-tools.
  • A maintenance window and an alternative access method (e.g. provider console).
  • Complementary checklist: see the VPS security checklist.
sudo apt update
sudo apt install -y clang llvm libbpf-dev libelf-dev build-essential \
  linux-headers-$(uname -r) bpftool iproute2 git make pkg-config xdp-tools

Write a basic XDP mitigation program (C)

The program below does minimal parsing of Ethernet and IPv4. It drops packets with invalid source IPs (RFC1918, loopback, multicast and reserved) and IP fragments. It also offers an option to drop UDP to ports 80/443 during attacks. Drop counters are kept per CPU.

Adjust the macros at the top to match your environment. If you use QUIC (UDP/443), disable the UDP drop option.

// xdp_ddos_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>

// Ajustes rápidos
#define DROP_UDP_TO_HTTP 1   // 1 = drop UDP para portas 80/443; 0 = permitir
#define DROP_FRAGMENTS   1   // 1 = drop de fragmentos IP; 0 = permitir

struct counters { __u64 packets; __u64 bytes; };

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, struct counters);
} drop_stats SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 16384);
    __type(key, __u32);   // IPv4 (host-order)
    __type(value, __u8);  // 1 = bloquear
} blocked_ips SEC(".maps");

static __always_inline int count_drop(__u32 len) {
    __u32 key = 0;
    struct counters *c = bpf_map_lookup_elem(&drop_stats, &key);
    if (c) {
        __sync_fetch_and_add(&c->packets, 1);
        __sync_fetch_and_add(&c->bytes, len);
    }
    return XDP_DROP;
}

static __always_inline bool is_bad_src_v4(__u32 s) {
    // s em host-order
    // 10/8
    if ((s & 0xFF000000) == 0x0A000000) return true;
    // 172.16/12
    if ((s & 0xFFF00000) == 0xAC100000) return true;
    // 192.168/16
    if ((s & 0xFFFF0000) == 0xC0A80000) return true;
    // 127/8 (loopback)
    if ((s & 0xFF000000) == 0x7F000000) return true;
    // 0/8
    if ((s & 0xFF000000) == 0x00000000) return true;
    // 169.254/16 (link-local)
    if ((s & 0xFFFF0000) == 0xA9FE0000) return true;
    // 224/4 (multicast) e 240/4 (reservado)
    if ((s & 0xF0000000) == 0xE0000000) return true;
    if ((s & 0xF0000000) == 0xF0000000) return true;
    return false;
}

SEC("xdp")
int xdp_ddos(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
        return XDP_PASS; // só IPv4 aqui

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    // Drop por fragmentação
    if (DROP_FRAGMENTS) {
        if (ip->frag_off & bpf_htons(IP_MF | IP_OFFSET))
            return count_drop((__u32)(data_end - data));
    }

    __u32 saddr = bpf_ntohl(ip->saddr);
    if (is_bad_src_v4(saddr))
        return count_drop((__u32)(data_end - data));

    // Blocklist dinâmica (hash)
    __u8 *blocked = bpf_map_lookup_elem(&blocked_ips, &saddr);
    if (blocked && *blocked == 1)
        return count_drop((__u32)(data_end - data));

    // Regras UDP opcionais
    if (DROP_UDP_TO_HTTP && ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void *)(ip + 1);
        if ((void *)(udp + 1) > data_end)
            return XDP_PASS;
        __u16 dport = bpf_ntohs(udp->dest);
        if (dport == 80 || dport == 443) {
            return count_drop((__u32)(data_end - data));
        }
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Compile the program to eBPF

Compile the eBPF object with clang. The resulting xdp_ddos_kern.o will be loaded onto the network interface. Run as root or with sudo. If you change the macros, recompile before reloading.

cat > xdp_ddos_kern.c <<'EOF'
[cole aqui o código C da seção anterior]
EOF

clang -O2 -g -target bpf -c xdp_ddos_kern.c -o xdp_ddos_kern.o
ls -lh xdp_ddos_kern.o

Attach the XDP program to the interface (native or generic)

Identify the VPS WAN interface. Try native (driver) mode first. If the driver doesn't support XDP, use the generic (skb) mode. Generic works in most cases but has higher overhead.

Warning: attaching XDP can break active connections. Have an alternate SSH or console ready. For safer access, consider an SSH bastion with 2FA. If you prefer, the xdp-tools package includes an xdp-loader.

IFACE=eth0   # ajuste se necessário
ip -brief link

# Modo nativo (driver). Se falhar, tente o genérico abaixo.
sudo ip link set dev "$IFACE" xdp obj xdp_ddos_kern.o sec xdp

# Alternativa: modo genérico (fallback)
# sudo ip link set dev "$IFACE" xdpgeneric obj xdp_ddos_kern.o sec xdp

How to confirm it worked (status and counters)

Check that the XDP program is attached and collect drop counters. Generate test traffic. In production, monitor the numbers and tune rules. For charts, see Prometheus & Grafana on VPS.

# Verificar XDP na interface
ih=$(ip -details link show dev "$IFACE" | sed -n '/prog/xdp/,$p')
echo "$ih" | sed -n '1,15p'

# Listar programas e mapas no netns
yes | sudo bpftool net -a

# Achar ID do mapa de contadores e exibir
STATS_ID=$(sudo bpftool map show | awk '/drop_stats/ {print $1}' | sed 's/id //')
echo "Mapa drop_stats id=$STATS_ID"
sudo bpftool map dump id "$STATS_ID"

Dynamically block IPs with bpftool (during an attack)

The blocklist works in real time. To add a suspicious source IP, update the hash map. Use the blocked_ips map ID. Remove the entry when the attack ends. Avoid broad blocks that impact legitimate clients.

# Descobrir o ID do mapa blocked_ips
MAP_ID=$(sudo bpftool map show | awk '/blocked_ips/ {print $1}' | sed 's/id //')
echo "blocked_ips id=$MAP_ID"

# Exemplo: bloquear 203.0.113.45 (hex: CB 00 71 2D)
sudo bpftool map update id "$MAP_ID" key hex CB 00 71 2D value hex 01

# Conferir
sudo bpftool map lookup id "$MAP_ID" key hex CB 00 71 2D

# Remover da blocklist
# sudo bpftool map delete id "$MAP_ID" key hex CB 00 71 2D

Make it persistent with systemd (load at boot)

Create a directory for the object and a systemd unit that attaches XDP after the network is online. Adjust IFACE in the unit file. To update rules, recompile and restart the service.

Caution: incorrect changes can lock you out at boot. Keep provider recovery access available.

sudo mkdir -p /opt/xdp
sudo cp xdp_ddos_kern.o /opt/xdp/

sudo tee /etc/systemd/system/xdp-ddos.service > /dev/null <<'UNIT'
[Unit]
Description=XDP DDoS mitigation (attach program on interface)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ip link set dev eth0 xdp obj /opt/xdp/xdp_ddos_kern.o sec xdp
ExecStop=/usr/sbin/ip link set dev eth0 xdp off

[Install]
WantedBy=multi-user.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now xdp-ddos.service
sudo systemctl status xdp-ddos.service --no-pager

Common troubleshooting and next steps

Some recurring failures can be worked around with quick checks. If nothing loads, test generic mode, validate kernel headers and do a clean rebuild. Combine XDP with additional layers such as an upstream L7 firewall and rate limits. To ban login attempts, use fail2ban with webhooks.

  • ip link: "Operation not supported": driver has no native XDP — use xdpgeneric.
  • bpftool: "map not found": confirm the program is attached and map names.
  • No counters: generate traffic or insert an IP into blocked_ips.
  • Lost remote access: detach via console: ip link set dev IFACE xdp off.
  • Target uses HTTP/3: if you use QUIC, disable DROP_UDP_TO_HTTP and recompile.
# Remover o programa XDP em caso de emergência
sudo ip link set dev "$IFACE" xdp off || sudo ip link set dev "$IFACE" xdpgeneric off

Frequently asked questions

Does eBPF/XDP replace an operator or CDN anti-DDoS service?

No. XDP mitigates on the host with minimal latency, filtering packets before the network stack. It helps against malformed, spoofed and unwanted UDP traffic, but it does not remove the load on the network link. Volumetric attacks require upstream mitigation.

Can I use XDP if my NIC doesn't support driver mode?

Yes. Use generic (skb) mode, which runs after packet reception and carries higher overhead. The ip link command offers the "xdpgeneric" option. If native fails, generic is a functional fallback for VPSs.

What types of attacks does this example help mitigate?

It blocks common spoofed sources (RFC1918, loopback, multicast and reserved ranges), discards IP fragments and optionally drops UDP to HTTP ports. It's useful against basic floods. You can extend the logic with per-IP rate limits and external blocklists.

How do I monitor XDP counters in production?

Use bpftool to read maps periodically and export the values to your metrics system. A simple option is a script that collects the maps and sends them to an exporter. For visualization, see the Prometheus & Grafana guide for VPS.

Is it safe to block UDP on port 443?

Only during an attack and only if you don't use QUIC/HTTP/3. Blocking UDP/443 will break QUIC. If your clients use HTTP/3, disable the corresponding macro in the code, recompile and reload the program.

Need help adapting XDP to your real traffic?

If you'd like assistance tuning rules, integrating metrics and automating the blocklist during incidents, we can discuss your environment and goals.

Talk about XDP mitigation

See also