GitHub

In-kernel data plane

Every other mitigation method Kapkan has is a request. RTBH and FlowSpec ask your routers to drop traffic; diversion asks them to send it somewhere else. All three depend on a peer that is willing and able to act on the announcement.

The data plane drops packets itself. Kapkan loads a small XDP program into the Linux kernel of the machine it runs on, and the rules the detector already builds for FlowSpec are written into kernel maps instead of being announced. Nothing has to propagate, and no peer has to accept anything.

This page is the overview. It answers three questions in order — is this for me, does it work, and where do I go next — and the rest of the section is the how:

  • Installing the data plane — the kernel and capability requirements, the config block, and how to bring it up safely in dry-run.
  • Operating & monitoring — reading its status, the counters and metrics to watch, the one alert you must set, and what to do when something is wrong.
  • Tuning & reference — the packet-evaluation order, static policy, per-source rate limiting, attach modes, and the map-sizing limits.

Is it for you? The one question that decides it

The data plane can only drop packets that reach the machine Kapkan runs on. That single fact decides whether the feature applies to your deployment, so answer it before anything else.

It helps when Kapkan runs in the traffic path:

  • on a Linux border router or a bump-in-the-wire box the traffic already crosses;
  • on the host being attacked, protecting itself;
  • on a dedicated scrubbing node that diversion steers traffic to.

It does nothing when Kapkan runs off-path — the classic deployment, where a Kapkan VM receives NetFlow or sFlow from your routers and speaks BGP back to them. No attack traffic flows through that VM, so there is nothing there for it to drop. In that topology, keep using RTBH and FlowSpec, and reach for the data plane only on a scrubbing node.

!It cannot un-saturate an upstream link

An XDP filter runs on packets that have already arrived on this machine's NIC. If a flood has already filled the pipe upstream of the box, the link is congested before the filter sees a packet, and dropping locally changes nothing. That is what the heavier, upstream steps are for. In an escalation ladder the data plane is the most precise response — not a replacement for the ones that move the drop upstream.

Does it work? Measured, not asserted

Every release replays eighteen attack captures end to end — synthetic telemetry into the real detector and classifier, the rules it generates compiled into real kernel maps, then the captured frames replayed through the program. Each capture runs a stream of legitimate traffic alongside the attack, because a block rate measured without one means nothing.

Measured over 18 capturesResult
Attack traffic dropped100% on 17 of 18; 98.5% on the per-source rate-limit capture, where each source is allowed its first frame before its bucket empties
Legitimate traffic droppedzero, in all 18
Allowlisted traffic droppedzero, in all 18

The captures cover UDP and SYN floods over IPv4 and IPv6; DNS, NTP, CLDAP, SSDP, memcached and chargen amplification; ICMP and fragment floods; a TCP ACK flood; a multi-vector mix; a carpet-bombed /24; a source flood; VLAN-tagged frames; and IPv6 with extension headers. The full suite also runs on real 5.15, 6.1, 6.6 and 6.12 kernels on every change.

iA block rate is not a throughput number

These numbers say what fraction of an attack the rules catch. They say nothing about how many packets a given box can absorb — that depends on your NIC, driver and CPU, and on whether the program attached in native or generic mode. Size a deployment on your own hardware; do not read capacity off this table.

How it fits the rest of Kapkan

The data plane is a mitigation method, alongside RTBH, FlowSpec and diversion, and it inherits everything the others do. Detection is unchanged. The safety model — dry-run, whitelists, TTLs and blast-radius caps — applies exactly as it does to a BGP announcement, because the data plane plugs in at the same point. What changes is only the last step: instead of an announcement leaving the box, rules are written into the kernel.

Severity runs none < dataplane < flowspec < divert < blackhole. On an escalation ladder a dataplane rung is the gentlest real action — drop locally first, and escalate to the router-based methods only if that was not enough.

Source blocks: from your own stack

Everything above is Kapkan deciding. There is one more way rules get into the kernel: something you run decides, and Kapkan enforces. An nginx in front of the victim, a log exporter, or an operator at 3am can hand over a source to drop, scoped to one victim, with a mandatory TTL:

curl -fsS -X POST localhost:8080/api/v1/dataplane/sources \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"victim":"203.0.113.10","source":"198.51.100.7","ttl_seconds":600,"reason":"login flood"}'

This is HTTP awareness without parsing HTTP. Kapkan never sees the requests; the thing that already terminates them makes the call, and the drop happens at the cheapest layer that can express it. Detection is not involved — no threshold has to trip, and nothing is classified.

The pair lives in the kernel with its own expiry, so it lapses on schedule even if Kapkan dies. POST /api/v1/dataplane/sources/unblock is the immediate undo. Full request shape, response and every refusal: the source-block channel.

The limits, because they are shared. Each blocked source claims one kernel policy slot from the same pool bans draw on, and holds at most 8 victims in it. Distinct sources are capped at what is left after every possible ban — host and carpet — could claim its own slot, so a burst of blocks can never starve a ban into its blackhole fallback. Past that, blocks are refused with 409 while bans keep working. Raise dataplane.limits.max_dynamic_rules if you intend to lean on this channel; watch kapkan_mitigate_source_blocks against that limit.

!The 8-victim cap is per source, across all tenants

One source's pairs share one kernel policy block, and that block is keyed by the source alone — it has no notion of which tenant each victim belongs to. So if the same attacker hits victims in several tenants, those victims compete for the same 8 slots, and the ninth is refused no matter whose it is. This is structural to the one-policy-per-source layout, not a limit that can be raised. Where that matters, keep source blocks to the tenant that reported the traffic, or fall back to a static rule.

Two refusals surprise people, and both exist because the alternative is silence: a source in dataplane.allowlist and a victim in protected_whitelist are errors, not accepted no-ops. The datapath passes both before any rule is evaluated, so such a block would install and then never match a packet. A source inside your own networks is refused too — an internal host is a ban, not a source block.

The reference exporter: kapkan nginx-exporter

You do not have to write the caller yourself. The binary ships one — a supported component, not an example — that tails an nginx access log, measures each source's request rate (and 4xx/5xx share) against a victim per window, and posts the verdicts to the channel above:

KAPKAN_API_TOKEN=... kapkan nginx-exporter \
  -log /var/log/nginx/kapkan.json.log \
  -api http://127.0.0.1:8080 \
  -rps 50 -min-requests 100 -window 10s -ttl 5m

It needs one thing from nginx — a JSON access log with the source, the destination and the status:

log_format kapkan escape=json
    '{"src":"$remote_addr","dst":"$server_addr","status":"$status"}';
access_log /var/log/nginx/kapkan.json.log kapkan;

Extra fields are fine (it ignores them), dst is the victim each block is scoped to (-victim overrides it when $server_addr is not the protected address), and -error-ratio 0.9 narrows verdicts to sources whose traffic is mostly rejects — leave it at 0 to catch well-formed floods too. -observe runs the whole loop but posts nothing, logging what it would block: the trial mode for a brain that is already live (the brain's own dry_run covers the deployment-wide case).

Some grounding for what it is: a fixed threshold an operator wrote, applied where requests are visible — not a detector (no baselines), not a WAF (it never reads request content — source, destination, status, nothing else). Every guarantee stays brain-side and every verdict is audited there like any operator's; the exporter is an ordinary API caller and can bypass nothing. It never reads log history on start, follows logrotate (create-new rotations are drained to the last line the old file held at the switch; what can still be missed is what nginx writes to the rotated file after the switch, until nginx's own reopen — typically milliseconds; copytruncate additionally has an inherent detection window of one 200ms poll, so prefer create-new), and a persistently hot source is refreshed before its TTL lapses — which is why -ttl must be at least twice -window, enforced at startup.

!src must be the socket's address — and the socket must be the client's

The block aims at whatever the log's src says, so src must stay $remote_addr — the address of the TCP connection nginx accepted. Never substitute a header-derived address (X-Forwarded-For, a realip value taken from an untrusted header): headers are attacker-written, and an attacker who controls src chooses what gets blocked. And if this nginx sits behind a load balancer or CDN, $remote_addr is that balancer — a "block" would cut the front door off for everyone. Run the exporter only on the box where real client connections terminate.

The token it needs is an operator token — a full operator credential for its tenant, not an exporter-shaped one: whoever holds it can also ban and unban inside that tenant, so scope it to the tenant whose victims this box serves (see authentication), keep it in a root-owned env file, and treat the box as holding that power. For a brain that is not on the same machine, put the API behind TLS — the token otherwise crosses the network in cleartext, and the exporter says so loudly at startup.

Where to go next

New to it? Read Installing the data plane and bring it up in dry-run, then Operating & monitoring to learn what to watch. Reach for Tuning & reference when you want static policy, per-source rate limiting, or to understand exactly how a packet is evaluated.