
Detect Port Scans with eBPF
The eBPF port scanning detection approach shows how kernel-level programs can answer TCP SYNs without leaving the fast path.
I guide you through a compact XDP example that reads Ethernet, IPv4, and TCP headers in the kernel. You will see how a simple program spots a SYN and replies with a SYN-ACK to emulate an open service for validation.
The demo focuses on practical steps. It filters IPv4/TCP and a target range to reduce overhead. It swaps MAC, IP, and ports, then sends the packet back via XDP_TX so responses appear immediately on the network.
This write-up also flags safety gaps. The simple demo omits checksum fixes and uses a fixed sequence number. I explain where to add checksum recalculation, correct sequence handling, and user-space maps so you can run this safely alongside real services.
Key Takeaways
- You will build an XDP-based program that reacts to SYNs in the kernel fast path.
- Understand how SYN, SYN-ACK, and RST-ACK reveal open or closed services.
- Filter IPv4/TCP and a target range to cut traffic and keep accuracy.
- Modify and transmit packets from the kernel, but add checksums and proper sequence handling for safety.
- Use user-space maps for config and metrics and tools like bpftool to monitor activity.
What you’ll build and why it matters
Follow a short, repeatable example that captures SYN probes at the ingress and forwards events for monitoring.
You will build a small XDP program that reads Ethernet, IPv4, and TCP headers at the NIC. The program flags SYN-only probes in a target port range and emits events to user space via maps or perf events. You will confirm behavior with Nmap -sS and tune lists and thresholds.
Use simple tools to load and inspect the code. When you need HTTP-layer context, switch to syscall tracing with BCC. Keep server workloads safe by skipping established flows and sockets that listen.
- Minimal code path: check EtherType, IPv4 proto, TCP flags, and destination port.
- Use maps so user space sets ranges, allowlists, and thresholds without rebuilds.
- Expose counters per source IP, per port, and per interface for monitoring dashboards.
| Action | Component | Command / Example | Result |
|---|---|---|---|
| Load | XDP program | ip link set dev eth0 xdp obj xdp.o | Program attached at NIC ingress |
| Verify | Events | bpftool map lookup name events key 1 | Receive information about SYN probes per IP |
| Test | Scanner | nmap -sS -p 80-90 target | Compare open vs closed behavior in server logs |
How SYN scans work at the TCP layer
A client begins a TCP session by sending a SYN. This is a single short packet with the SYN flag set. It contains only a few bytes of header information.
A listening service replies with a SYN-ACK. That reply tells the client the socket is ready. The server also allocates an entry in the SYN queue for a short time.
The client completes the three-way handshake by sending an ACK. Only then does the server move the entry into the accept queue and hand it to the application.
Open vs closed behavior
If the port is closed, the host sends a RST-ACK. That response allocates no state. It is fast and cheap for the remote system.
- A TCP session starts with one SYN packet of a few bytes.
- Listening sockets reply SYN-ACK and use the SYN queue briefly.
- The final ACK completes the handshake and creates the socket for the app.
- Closed services answer with RST-ACK and allocate no state.
Nmap -sS sends a SYN and reads the next packet to decide open (SYN-ACK) or closed (RST-ACK). It then stops before sending the final ACK. This reduces logging for simple filters.
From a system view, the pattern is many SYN packets without matching ACKs. The function of our code is to count and classify that pattern over time.
Choosing an eBPF approach: XDP packet path vs syscall tracing
Decide between NIC-level filtering and syscall tracing by weighing speed against visibility. Each approach runs in the linux kernel but serves different needs.
When to use XDP for high-speed packet filtering
Pick XDP when you need line-rate handling on heavy traffic. It runs at NIC ingress and checks only L2–L4 headers. That keeps per-packet cycles tiny.
- Best for millions of packets per second.
- Supports XDP_PASS, XDP_DROP, and XDP_TX for fast replies.
- Programs stay small and focus on header logic to reduce overhead.
When to trace system calls for application-layer visibility
Use syscall tracing when you need app context. Hook accept4, read, write, and close to reconstruct flows and capture HTTP-level data.
- BCC or libbpf lets you link read/write events to file descriptors.
- Tracing emits structured records for timing and function outcomes.
- Good for troubleshooting and payload-aware analysis.
In practice, combine both. Run XDP to flag high-rate anomalies and a tracing program to sample suspicious connections. Keep each program focused. Test changes in a lab to avoid accidental drops or latency spikes.
Plan the eBPF/XDP program for network traffic inspection
Start by defining simple header checks so the kernel work stays light and predictable.
Parse the Ethernet header and check the EtherType equals IPv4. If it is not IPv4, return XDP_PASS to avoid wasted cycles.
Identify IPv4 and TCP packets in the kernel
From the IPv4 header, confirm the protocol is TCP. Pass UDP and ICMP quickly. Use bounds checks before reading the TCP header so the verifier accepts the code.
Target ports and filter ranges without excess overhead
Extract the destination port and compare it against a range, for example 9000–9500. Use a hash map for watched ports so you can update the set from user space.
- Keep a per-source counter in a map for SYN-only attempts.
- Store only small items: flags, ports, and counts. Avoid copying payload data.
- Use a config map to toggle simulation mode and prevent interference with real services.
- Write the code in clear, small functions so the verifier can follow types and control flow.
| Step | Header check | Action | Why |
|---|---|---|---|
| 1 | EtherType == IPv4 | XDP_PASS otherwise | Saves CPU by skipping non-IP types |
| 2 | IPv4 proto == TCP | XDP_PASS otherwise | Keeps program small and safe |
| 3 | TCP dest port in 9000–9500 or map | Update per-src counter | Allows user-space thresholds and response |
When you need examples for user-space maps and helpers, see this short guide to start programming with BCC tools: start with BCC tools.
Implement the XDP logic: filter, detect SYN, respond
I will walk you through a compact XDP flow that filters packets, flags SYN-only attempts, and crafts a SYN-ACK reply from the kernel fast path.
Keep every read and write guarded by bounds checks. Get mutable pointers to the Ethernet, IPv4, and TCP headers. Verify EtherType == IPv4 and IPv4 proto == TCP before any header access.
- Use a map or a numeric range (9000–9500) to filter destination ports. This limits work to relevant traffic.
- Detect SYN-only packets by testing SYN==1 and ACK==0. Pass other packets to avoid breaking real sessions.
- Swap L2 MACs. Swap L3 IPv4 addresses. Swap TCP source and destination ports.
- Set the TCP ACK flag. Compute ack_seq = seq + 1. Set seq = 1 (use a secure initial sequence in production).
- Update a counter map for packets inspected and responses sent. This helps measure traffic and tune thresholds.
- Return XDP_TX so the modified packet goes back out on the same interface with minimal time in the kernel.
Concrete header manipulations in code follow verifier-friendly patterns: check lengths, then cast to EthHdr*, Ipv4Hdr*, and TcpHdr*. Do not access fields before checks pass.
| Action | Header Field | Value/Operation |
|---|---|---|
| Ethernet | dst/src MAC | Swap source and destination MAC addresses |
| IPv4 | src/dst IP | Swap and preserve TTL; recalc header checksum in prod |
| TCP | flags, seq, ack_seq, ports | Set ACK, ack_seq=in_seq+1, seq=start_seq; swap ports; recalc checksum |
Note the checksum and sequence caveats. Test setups and some raw sockets let you skip checksum fixes, but production code must recalculate IP and TCP checksums and use secure initial sequence numbers. Skipping those steps can cause peers to drop the response or create subtle runtime issues.
Coordinate with user space for control and visibility
Keep kernel work minimal and let a small user process handle configuration and aggregation. Run simple, verifier-friendly programs in the kernel and move stateful logic to user space. This reduces risk and gives you flexible control.
Use maps for lists, thresholds, and counters. Pin maps in bpffs so file-backed maps survive reloads. Send compact events to user space with a ring buffer or perf output. A small user agent reads events and writes JSON lines for easy parsing.
Practical setup
- Keep program size small and store toggles in maps writable from user space.
- Use perf/ring buffer to emit events with timestamp, src IP, dst port, and code.
- Store counters per source IP and per port and export via a Prometheus exporter.
- Pin config and metrics as separate files in bpffs to avoid accidental loss.
- Bound map sizes and use LRU maps to limit memory use under load.
| Item | Location | Purpose |
|---|---|---|
| Config map | bpffs pinned file | Editable list of simulated open ports and toggles |
| Event ring | perf/ring buffer | Compact events to user space for logging and monitoring |
| Counters map | pinned map | Per-src and per-port metrics for exporter |
Validate user space writes before accepting them. Run the user process with least privilege and only the caps it needs. This keeps the system safe while giving you good monitoring and control.
Validate results with nmap and real traffic
Run a controlled scan and compare outputs to confirm your kernel reply logic works as intended.
Use this exact command from a tester machine or the server itself:
sudo nmap -sS -p9000-9500 <your_ip>As root, nmap uses raw sockets and shows whether the server replies with SYN-ACK or reset. If your program crafts SYN-ACK replies, nmap will report those ports as open. If the host sends a reset, the ports show as closed.
Quick interpretation and tuning
- Run the command before and after loading the program to compare output and confirm changes.
- Capture a few packets with tcpdump on a mirrored interface to see SYN and SYN-ACK pairs for packet-level analysis.
- If nothing changes, verify the program is attached to the correct interface and the IP used in the command is correct.
- If peers ignore replies, check checksum offload or missing recalculation as the likely cause.
- Adjust the simulated range in your config map and rerun the scan to confirm dynamic updates.
| Action | Command / Example | Expected output |
|---|---|---|
| Scan test | sudo nmap -sS -p9000-9500 <your_ip> | Ports in simulated range: open. Others: closed/reset. |
| Packet capture | sudo tcpdump -i mirror ‘tcp[tcpflags] & (tcp-syn) != 0’ | See SYN from scanner and SYN-ACK from server/program. |
| Troubleshoot | bpftool show; ip link show | Confirm program attached and correct interface selected. |
Production hardening and safety checks
Production use demands strict safeguards to prevent kernel regressions and service disruption.
Recalculate checksums and handle sequence numbers
Always recompute IP and TCP checksums when you change headers. Many stacks drop packets with bad checksums. Use cryptographic-quality initial sequence numbers. A fixed value is acceptable only in a lab.
Avoid impact on legitimate servers and sockets
Do not simulate on ports with real services. Detect established flows and pass them unchanged. Limit attachment to interfaces that need monitoring to reduce blast radius.
Gate loading with least privilege and policy
Require signed binaries and restrict who can load kernel objects. Set CONFIG_BPF_UNPRIV_DEFAULT_OFF and avoid CONFIG_BPF_KPROBE_OVERRIDE. Limit SYS_bpf usage and enforce role-based policies.
| Safeguard | Action | Why it matters |
|---|---|---|
| Checksum recompute | Recalc IP/TCP after edits | Prevents packet drops by peers |
| Secure ISNs | Use crypto-quality sequences | Protects against injection and prediction |
| Scope limits | Attach per-interface only | Reduces kernel exposure and risk |
| Policy & signing | Signed files; RBAC for loading | Controls who can change runtime behavior |
| Memory tracking | Monitor map sizes and usage | Prevents OOM and degradation on busy servers |
Keep a rollback runbook. Pin maps to a file, keep prior binaries, and include a detach command. Review each code change. Small edits can change verifier results and runtime safety. These steps protect your servers and improve overall security.
Detect and monitor eBPF activity on the host
A quick inventory of loaded ebpf programs and pinned maps makes audits practical and repeatable. Start with simple commands to collect baseline information and schedule regular checks.
Use bpftool to list programs, maps, and links
Run these commands and note unexpected entries, sizes, or pins.
bpftool prog show— lists program IDs, types, and attach points. Confirm your XDP entry and any unfamiliar programs.bpftool map show— shows maps, key/value sizes, and pin paths. Watch for large maps or unknown files in /sys/fs/bpf.ip link show dev <iface>andtc filter show dev <iface>— verify interface attachments and hooks.
Tracee, Falco, and LKRG for runtime events and integrity
Deploy Tracee to log bpf() loads and attach events with timestamps and PIDs. Add Falco rules to alert on unplanned bpf(), perf_event_open, or new pins. Install LKRG to guard kernel structures and block tampering.
| Action | Command | What to check |
|---|---|---|
| Inventory | bpftool prog show | Unknown program types or attach points |
| Maps | bpftool map show | Large maps or unexpected pin files |
| Audit | Tracee / Falco | Suspicious events and process context |
Schedule nightly diffs of bpftool output and bpffs listings. Centralize logs so you can link events to sockets and processes. Document expected programs and files so operators spot anomalies quickly.
Handling adversarial use: eBPF backdoors and stealth scans
Kernel-resident backdoors can hide in memory and never touch a file. Traditional AV looks at files on disk. That makes it blind to programs that live only in kernel memory.
Act early. Alert on bpf() calls and unexpected program attachments with Tracee or Falco. Those tools catch loads at the syscall level before an implant can tamper with bpftool output.

If you suspect compromise — practical triage
- Snapshot the virtual machine memory from the hypervisor.
- Run Volatility with eBPF plugins to extract program metadata and map state.
- Save bpftool output and network logs to a file for later correlation.
Cloud and hypervisor notes
In OpenStack, run a small script from the host to SSH into the guest and run a bpftool-based check. Example: openstack server ssh <vm> –command ‘sudo ./scan_bpf.sh > /tmp/bpf_output.txt’.
In vSphere with NSX, you cannot inspect guest kernel memory from the network. Use distributed firewall rules, IDS/IPS, and flow analytics to flag odd east-west traffic between servers.
| Step | Action | Why |
|---|---|---|
| Load-time alert | Tracee / Falco watch bpf() calls | Most reliable initial breakpoint |
| Memory forensics | Hypervisor snapshot + Volatility | Extract in-memory program and map state |
| Network signs | NSX flow analytics, IDS/IPS | Detect hidden sockets or odd traffic paths |
Keep a short checklist and a small toolkit on hand: Tracee, Falco, bpftool, Volatility. Train responders to link process IDs, program IDs, and traffic quickly. Limit kernel capabilities on production images to reduce the attack surface.
Next steps to apply this on your servers
Deploy the XDP program in a controlled lab first. Attach it to one machine and set a small port range in the pinned map. Run a scan with this command to verify behavior: sudo nmap -sS -p9000-9500 <your_ip>.
Add checksum recomputation and secure sequence numbers in your code before moving beyond tests. Pin maps in bpffs so you can update a file without reloads. Build a small user space helper to push config, read events, and export metrics to your monitoring stack.
Keep a runbook with attach/detach and state checks. Use bpftool to list programs and maps. Enable Tracee and Falco rules and register LKRG on images. Review logs weekly and keep a single-command rollback to detach the program if needed.
