Programmable Packet Filtering: Bringing eBPF to Magic Firewall

Magic Transit customers rely on Cloudflare to absorb DDoS attacks while Magic Firewall applies their custom packet-level rules at every server in every data center. Attack sophistication keeps rising—recent campaigns against VoIP providers targeting Session Initiation Protocol (SIP) are a case in point. Pushing back requires filtering logic that goes beyond what a conventional firewall rule set can express.

The existing Magic Firewall implementation is a distributed stateless packet firewall built on Linux nftables. Each customer's nftables rules live in their own Linux network namespace, which keeps configurations isolated from one another. Users author rules through a single API with Wirefilter syntax.

This diagram shows how packets are processed by Magic Firewall on a Cloudflare server.

The packet path for Magic Transit users runs through DDoS protection first, then into the customer-specific namespace where nftables rules are applied, and finally out via a GRE tunnel toward the origin. This setup gives customers powerful matching primitives, but only those nftables provides out of the box. For deeper packet parsing and content-based matching, something more was needed.

Extending nftables with xt_bpf

eBPF is the natural fit for adding programmable, in-kernel packet processing. Cloudflare already relies on eBPF across several products, and the goal here was to let an eBPF program act as a match within an existing nftables table and chain. That would keep all the existing infrastructure and syntax intact while layering on new capabilities.

nftables does not natively support eBPF programs as match criteria. iptables does, via the xt_bpf extension, which accepts a BPF_PROG_TYPE_SOCKET_FILTER program. An iptables rule with a pinned eBPF program can drop packets like this:

iptables -A INPUT -m bpf --object-pinned /sys/fs/bpf/match -j DROP

Since iptables and nftables both talk to the kernel over the nftables API, it is possible to configure xtables matches—including xt_bpf—inside an nftables rule set even though nftables itself has no direct notion of eBPF.

BLOG-849 Embedded Image - BzET9q

The key was figuring out the correct netlink/netfilter messages to embed an xt_bpf match in an nftables rule. Working from the kernel source, strace traces, and packet captures, the team constructed the message structure needed to append an eBPF-based rule to a table and chain.

NFTA_RULE_TABLE table
NFTA_RULE_CHAIN chain
NFTA_RULE_EXPRESSIONS | NFTA_MATCH_NAME
	NFTA_LIST_ELEM | NLA_F_NESTED
	NFTA_EXPR_NAME "match"
		NLA_F_NESTED | NFTA_EXPR_DATA
		NFTA_MATCH_NAME "bpf"
		NFTA_MATCH_REV 1
		NFTA_MATCH_INFO ebpf_bytes	

That message carries an ebpf_bytes payload whose format is defined by struct xt_bpf_info_v1 in the kernel headers. The structure supports two modes: raw bytecode or a path to a pinned eBPF program. The pinned-program mode is what makes the combination with nftables work in practice.

 struct xt_bpf_info_v1 {
	__u16 mode;
	__u16 bpf_program_num_elem;
	__s32 fd;
	union {
		struct sock_filter bpf_program[XT_BPF_MAX_NUM_INSTR];
		char path[XT_BPF_PATH_MAX];
	};
};

With the wire format decoded, the team could write code that serializes netlink messages correctly and injects eBPF-backed matches into nftables rules. Custom netfilter messages were only the starting point; proper tooling around this approach is also under consideration.

Building and Loading the eBPF Match

With the plumbing in place, the next step was writing an actual eBPF program. A sample program accepts only packets that carry a magic string at the end of the payload, which means checking total packet length to locate the search start point.

SEC("socket")
int filter(struct __sk_buff *skb) {
  /* get header */
  struct iphdr iph;
  if (bpf_skb_load_bytes(skb, 0, &iph, sizeof(iph))) {
    return BPF_DROP;
  }

  /* read last 5 bytes in payload of udp */
  __u16 pkt_len = bswap_16(iph.tot_len);
  char data[5];
  if (bpf_skb_load_bytes(skb, pkt_len - sizeof(data), &data, sizeof(data))) {
    return BPF_DROP;
  }

  /* only packets with the magic word at the end of the payload are allowed */
  const char SECRET_TOKEN[5] = "xyzzy";
  for (int i = 0; i < sizeof(SECRET_TOKEN); i++) {
    if (SECRET_TOKEN[i] != data[i]) {
      return BPF_DROP;
    }
  }

  return BPF_OK;
}

Several loading paths were evaluated: BCC, libbpf, and a custom loader. The team settled on cilium's ebpf library because the control plane is written in Go and the library makes it straightforward to generate, embed, and load eBPF programs. Once a program is compiled and pinned, netlink commands can add the match into an nftables table and chain, and the ruleset listing confirms its presence.

# nft list ruleset
table ip mfw {
	chain input {
		#match bpf pinned /sys/fs/bpf/mfw/match drop
	}
}

The result is a working path to deploy custom C programs as advanced matching logic inside a Magic Firewall ruleset, without replacing the nftables foundation.

What This Unlocks

With eBPF in the toolkit, Magic Firewall can inspect deeper into packets and run more complex matching logic than nftables alone would allow. Because the firewall is software running across all Cloudflare servers, new matching capabilities can be iterated quickly. The first product outcome is SIP protection, currently in beta. Protocol validation, advanced field matching, payload inspection, and larger IP list sets are all on the roadmap for eBPF-based enhancements.