We Put an L7 Firewall in the KernelJuly 6, 2026<br>9 min read
An L7 Firewall in the Kernel<br>By: Jason Evans
"This is some Jane Street sh*t." That was the reaction from a Director of Engineering at a Fortune 500 company.
Firewalls that decide on application data, like an HTTP header rather than only an IP address and a port, almost always run that decision in a userspace proxy. The packet gets copied out of the kernel, the TCP stream gets rebuilt, the inspection engine runs, and the result gets copied back down. It works, and it costs milliseconds per request.
We thought that layer could be faster and easier to run. So we built a firewall that makes its decision in the kernel with eBPF, and lets you write the policy as a JavaScript app. Sounds fun, right?
It was. The decision lands in the nanoseconds, and since the policy is just a JavaScript app, changing it takes no rebuild, redeploy, or restart. It's already running in front of real enterprise traffic. Here's how it works and why we built it this way.
Why the kernel can do this now
Deciding on HTTP in the kernel used to be impractical, and not by accident. The eBPF verifier rejects any program whose loops it can't prove will terminate, which kills most of the parsing you'd reach for on instinct. Earlier work got part of the way there but stayed narrow: one 2024 study of L7 header parsing in eBPF could match about 48 bytes of payload, nowhere near enough to decide on a real HTTP/2 header block.
That ceiling has moved. Recent kernel work pushed in-kernel L7 matching from tens of bytes into the kilobytes, and we aren't the only ones building on it. In May 2026 a team from ETH Zürich, NYU, Nvidia, and Politecnico di Milano published Offloading L7 Policies to the Kernel (L7FP), which synthesizes eBPF data planes that enforce most real service-mesh L7 policies in the kernel. The paper is useful to us for two findings, neither of them ours. They surveyed 2,417 open-source projects and found 89% of deployed L7 policies run in eBPF with no kernel changes, which puts in-kernel L7 at most of the workload rather than some edge case. And to parse inside the verifier's limits, they landed on Aho-Corasick DFAs. Coincidentally our CEO and architect, Julian Goldstein, had landed on this approach before we discovered the published paper, for the exact same reason.
What it decides on
A normal packet filter matches on the 5-tuple: source IP, destination IP, source port, destination port, protocol. That is layer 3 and 4.
Ours matches on values inside HTTP/2: the :authority (host), the User-Agent, and the source address, with X-Forwarded-For and PROXY protocol resolution when it sits behind a trusted load balancer. Rules combine those with boolean logic, for example:
host == "api.example.com" && src in 127.0.0.0/8 && ua ~ "Diffbot"
A common use today is blocking crawlers and agents by User-Agent (GPTBot, ClaudeBot, PerplexityBot, curl/, and so on) on specific hosts, with a source constraint so a rule only applies where you expect that traffic to come from.
Where the decision runs, and why there
The match runs at XDP, the eBPF hook in the NIC driver, before the kernel allocates a socket buffer and before the packet touches the network stack. The verdict, pass or drop, is produced right there.
We hook at XDP on purpose, and it's the biggest thing separating us from the academic work above. L7FP hooks at the socket layer, after the kernel has reassembled the TCP stream and decrypted TLS. That hands you a clean byte stream, which is nice. Hooking earlier means we reassemble the stream ourselves (more on that in the fragmentation section), and we pay that cost because XDP earns it back.
A packet that dies at XDP never becomes a socket buffer and never enters the stack. It's the cheapest drop Linux has: the kernel spends nothing on traffic we were going to reject anyway.
Enforcement also sits below userspace. You write and submit rules from JS, and the runtime compiles them into the structures the kernel program reads. All the expressive work, writing rules, managing config, streaming logs, lives in a normal JS app. The verdict itself runs in the kernel, where it's cheap and where a compromised userspace process can't quietly switch it off.
Updating rules is a git push. No rebuild, no redeploy, no restart. Nifty.
The matching, and why Aho-Corasick
Checking a User-Agent against a list of banned substrings one at a time means scanning the header once per pattern. Instead we compile every UA substring across every rule into one Aho-Corasick automaton: a DFA that reads the header a byte at a time and reports every pattern that hit in a single pass.
The scan is linear in the length of the header and doesn't slow down as you add patterns. Ten patterns or ten thousand, the header is still a couple hundred bytes and you make one pass over it. More rules cost table size, not scan time.
It's also the only shape that gets you into the kernel. The verifier only loads a...