It’s only been a little over a week since I first talked about oubliette, my little eBPF/XDP side project to combat spoofed TCP SYN floods. If you haven’t seen it yet, definitely check out the deep dive on the TCP handshake quirk that makes this whole thing possible.

I’ve finally carved out enough time to run a complete end-to-end demo case in the lab. And let me tell you, seeing this thing actually work in a distributed environment with multiple taps installed is pure joy.

No more theoretical diagrams. Let’s look at the actual packets.

The Lab Setup

The topology uses network namespaces, veth pairs, and Linux bridges to simulate a highly asymmetrical environment. Since we’re dealing with raw packets, let me break down exactly how the traffic flows and where the taps (the bracketed names in the traces) are placed.

flowchart LR
    Client((Client 10.88.0.10))
    Scrubber{Oubliette Scrubber}
    Edge[Edge Router]
    Dist[Dist Router]
    Server((Server 10.99.0.1))

    Client -->|tap: client to ingress-bridge| Scrubber
    Scrubber -->|tap: egress-bridge to dist-egress| Dist
    Dist -->|tap: dist-backend to backend-srv-side| Server

    Server -.->|tap: server to backend-srv-side| Dist
    Dist -.->|tap: dist-backend to dist-egress| Edge
    Edge -.->|tap: edge-egress-side to edge-ingress-side| Client

As you can see, legitimate inbound traffic flows through the Scrubber to the Dist Router, but the return traffic bypasses the Scrubber entirely and routes asymmetrically through the Edge Router back to the Client. This matches exactly how you’d deploy it in the real world to prevent stateful bottlenecks.

When oublietted starts up, it attaches the XDP program to the interfaces and immediately establishes BGP sessions with the upstream and downstream peers.

{"time":"2026-04-30T12:08:50.44904566Z","level":"INFO","msg":"XDP program attached","subsystem":"xdp","ingress_interface":"oescrI-n"}
{"time":"2026-04-30T12:08:51.524353096Z","level":"INFO","msg":"BGP session established","subsystem":"bgp","peer_addr":"172.16.88.3","role":"upstream","state":"ESTABLISHED"}

Once the BGP sessions are up and the routes are propagated, it’s go time. I ran three distinct scenarios to prove the challenge/response logic.

Legitimate / Forwarded TCP
Dropped / Blocked TCP (Spoofed)

Scenario 1: The Legit User

The whole premise of oubliette is that we send a garbage SYN-ACK to a new client. If it’s a real TCP stack, it replies with an RST. Once we see that RST, we whitelist the IP.

Let’s look at the raw packet trace for a legitimate connection from 10.88.0.10.

Time
Client
Oubliette
Backend Server

Look at that. Beautiful.

The client sends a SYN at 4.299s. The scrubber intercepts it at the ingress bridge and fires back the challenge SYN-ACK ([S.]).

Instantly, the client says “nope” and sends an RST ([R]).

oubliette catches that RST, promotes the IP to authenticated, and when the client retries mere milliseconds later at 4.302s, the SYN is seamlessly forwarded to the backend server. The backend replies with the real SYN-ACK, and the connection is established. It works exactly as designed.

Scenario 2: The Spoofed Flood

Now, what happens when an attacker blasts us with spoofed IPs?

In this scenario, I simulated a flood of SYN packets claiming to be from 203.0.113.77.

Time
Client
Oubliette
Backend Server

The scrubber sees the first SYN and issues the challenge SYN-ACK. Because the IP is spoofed, the real owner of 203.0.113.77 silently drops it. No RST is ever sent.

When the attacker sends subsequent SYNs, oubliette doesn’t even bother generating more challenges. It just drops them. Zero RSTs received, zero packets forwarded to the backend. The backend server remains completely unaware that it’s under attack.

Scenario 3: Chaos in the Mix

For the final test, I threw 100 spoofed SYNs (from dynamic IPs) at the scrubber concurrently with 10 legitimate connections. A 10:1 ratio of garbage to real traffic.

The results? Absolute perfection.

Time
Client
Oubliette
Backend Server

All 100 spoofed attempts failed to pass the challenge and were dropped at the NIC. All 10 legitimate connections replied with an RST, got whitelisted, and sailed straight through.

If we look at the Prometheus metrics from the daemon:

# HELP oubliette_xdp_packets_total Total number of packets processed by XDP
# TYPE oubliette_xdp_packets_total counter
oubliette_xdp_packets_total{action="drop",reason="unclassified"} 3
oubliette_xdp_packets_total{action="forward",reason="unknown"} 96
oubliette_xdp_packets_total{action="processed",reason="none"} 105

# HELP oubliette_syn_ips_promoted_total Total number of IPs promoted from unauthenticated to authenticated
# TYPE oubliette_syn_ips_promoted_total counter
oubliette_syn_ips_promoted_total{family="unknown"} 1

The XDP hook is doing its job exactly how we want it to—processing packets at linerate, forwarding the good stuff, and brutally dropping the unclassified noise.

Next Steps

I am beyond thrilled with how stable this is proving to be. Dealing with stateful verification while maintaining line-rate performance isn’t peanuts, but eBPF makes it feel almost easy.

I’ve still got some cleanup to do on the userspace sweeper logic, but the core engine is cooking.

We’re still on track for a late Q2 release. Can’t wait to see this running in the wild!


Appendix: Raw PCAPs

If you want to poke around in Wireshark and see the raw bytes for yourself, you can download the full set of packet captures from these tests here:

Download demo-pcaps.tar.gz