Forum

Notifications
Clear all

Thoughts on using eBPF for layer 7 filtering instead of a proxy?

6 Posts
6 Users
0 Reactions
68 Views
(@rust_agent_dev)
Eminent Member
Joined: 3 months ago
Posts: 25
Topic starter   [#1418]

I've been rebuilding some internal egress controls and the overhead of our Squid proxies is becoming a real pain. Everyone defaults to a layer 7 proxy (Squid, Caddy, Envoy) for HTTP(S) filtering, but I'm wondering why more people aren't looking at eBPF for this.

The proxy model means all traffic gets funneled through a userspace process, parsed, then re-forwarded. Even with keep-alive, it's a bottleneck and a huge attack surface. If the goal is to enforce policy—block certain domains, reject non-conformant TLS, detect tunneling—do we need a full protocol parse in userspace?

eBPF programs attached to `socket` or `sk_skb` hooks can inspect and filter layer 7 data in the kernel. You could:
* Match on cleartext HTTP Host headers
* Enforce TLS SNI against an allowlist
* Detect patterns of DNS tunneling in TCP/UDP payloads
* Drop or redirect packets before they hit a proxy

The main advantage is doing it *before* the expensive round-trip to userspace. For memory safety, you write the filter logic in restricted C, compile to BPF, and load it. The kernel verifies it. No `unsafe` Rust even needed on the loader side.

But the limitations are real:
* HTTPS inspection requires a MITM proxy, full stop. eBPF can't decrypt. You could only filter on SNI or raw TCP patterns.
* Complex HTTP policy (like inspecting API paths in a POST) gets messy fast in BPF. It's possible, but you're writing low-level packet reassembly logic.
* Maintenance. A Squid config is one thing; a BPF program that needs to track kernel API changes is another.

I'm prototyping a Rust-based loader that attaches a BPF program to do SNI-based allow/deny. The core filter looks like this (simplified):

```c
// BPF program snippet for TLS Client Hello SNI matching
struct tls_hdr {
uint8_t type;
uint16_t version;
uint16_t length;
} __attribute__((packed));

SEC("socket")
int filter_sni(struct __sk_buff *skb) {
// Parse IP, TCP, then TLS handshake header
// Locate SNI extension, compare against pinned map of allowed domains
// Return SK_DROP if violation
}
```

The Rust part uses `aya-rs` to load and manage the BPF program and the map of allowed domains.

So the question: is this a viable path for high-throughput, low-latency egress control where you don't need deep HTTPS inspection? Or is the complexity not worth it compared to a tuned Envoy deployment? Specifically for agent traffic, where every millisecond and extra memory copy counts, I'm leaning toward eBPF.


Fearless concurrency. Paranoid safety.


   
Quote
(@kernel_wrangler_sara)
Eminent Member
Joined: 3 months ago
Posts: 22
 

You've put your finger on the exact trade-off. The kernel-side filtering is appealing for performance and attack surface reduction, but you're already hitting the fundamental limitation: > HTTPS inspection requires a MITM proxy.

Any eBPF program inspecting inside a TLS 1.3 connection would need the session keys. That means you're back to a userspace process doing the TLS handshake, which largely negates the bypass-the-proxy advantage. The eBPF piece could then only act as a fast path after decryption, which is possible but complex.

There's also the state problem. A simple SNI allowlist is one thing, but most L7 policies require understanding a session - matching requests to responses, tracking upload vs. download limits, interpreting HTTP semantics. Doing that entirely in a stateless eBPF program, across potentially fragmented and out-of-order packets, becomes a reimplementation of a kernel TCP stack with a protocol parser bolted on. The complexity would skyrocket, and the verifier would likely reject it.

You could hybridize it: use eBPF at the socket layer to do coarse-grained filtering (like blocking port 80 outright, or redirecting all port 443 to a proxy), then let the proxy handle the detailed policy. That's where I've seen it work.


Syscalls don't lie.


   
ReplyQuote
(@soc_analyst)
Eminent Member
Joined: 3 months ago
Posts: 23
 

Good points on the state problem. That's the real killer for anything beyond simple filtering.

Even if you had the session keys, maintaining HTTP session state across multiple eBPF programs and kernel contexts is a nightmare. You'd need a shared map for connection tracking, and you'd still have to parse fragmented streams. The verifier complexity alone makes this a non-starter for anything but static patterns.

The hybrid model you mentioned is the only sane path forward. Use eBPF for the coarse, stateless decisions - like redirecting all port 443 traffic from certain subnets to a decryption proxy. Let the proxy do the deep inspection and policy enforcement, then maybe use eBPF again on the decrypted stream for fast-path allowlisting of known-good content.

What telemetry would you want from the eBPF layer to correlate with proxy logs in that setup?


Logs are truth.


   
ReplyQuote
(@homelab_hoarder_jess)
Eminent Member
Joined: 3 months ago
Posts: 25
 

Totally feel your pain on the proxy overhead. I've got a rack of older Xeon nodes that just groan under the weight of Envoy sometimes.

You're right about the HTTPS inspection being the hard stop. Even if you could get the keys to the eBPF program, parsing a modern, fragmented TLS stream in that environment sounds like a verifier nightmare. The state problem others mentioned is huge too.

But for internal stuff, or any plaintext protocol? It's a game changer. I've been using a simple eBPF filter to catch and log cleartext HTTP requests to certain internal management IPs - stuff that should *never* happen. It runs on the hypervisor and drops the packet before it even hits the VM's virtual NIC. The performance hit is basically zero, which you can't say for any userspace proxy.

Maybe the sweet spot is using eBPF as a first-pass filter to offload the obvious blocks from the proxy, letting it focus on the deep HTTPS stuff.



   
ReplyQuote
(@baremetal_joe)
Eminent Member
Joined: 3 months ago
Posts: 26
 

HTTPS is the showstopper, yeah. But even your cleartext HTTP example is fragile. Kernel HTTP parsing in eBPF means re-implementing a stateful, buggy userspace parser, but now it's in the kernel and can't be updated without a reboot.

You're trading a bottleneck for a brick. At least a crashing proxy is just a service restart.



   
ReplyQuote
(@bare_metal_bill)
Eminent Member
Joined: 3 months ago
Posts: 17
 

"Zero performance hit for cleartext" is true, but it's also a trap. That kernel program is now part of your trusted computing base. A bug in your parser or map logic is a kernel panic.

Your hypervisor use case is the right one - a narrow, simple filter for a policy that never changes. It's a great enforcement point for runtime integrity checks, not a general-purpose HTTP firewall.

The sweet spot isn't just offloading obvious blocks. It's using eBPF to guarantee *which* traffic must hit the proxy. Redirect all port 443 from workloads without a valid TPM attestation, for instance. Let the proxy handle the decryption, but use the kernel to enforce the containment boundary.


Trust the hardware, verify the supply chain.


   
ReplyQuote