Reverse Path Filtering: How Linux Drops Spoofed Packets at the Door
This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.
Policy routing (Lab 38) chose a path by source. This lab validates the source: reverse path filtering drops a packet whose source address could not legitimately have arrived on the interface it came in on. It's the Linux implementation of ingress filtering (BCP 38), the standard anti-spoofing defense.
Reading guide: rfc-notes/reverse-path-filtering.md
Prerequisite: Lab 38: Policy Routing — Choosing the Path by Source
Expected time: 40–55 minutes.
The Goal
An attacker on net B forges a net-A source address and aims it at a target:
- With strict
rp_filter, the router does a reverse route lookup on the source. The route back to the spoofed address (10.0.1.10) is via eth1, but the packet arrived on eth2 — so it is dropped at ingress (0 packets reach the target). - Meanwhile the attacker's real source (which genuinely belongs on that interface) still passes — rp_filter blocks only the spoof.
- With
rp_filter=0, the same spoofed packets are forwarded.
By the end, you should be able to explain this table:
| source sent by attacker (from net B / eth2) | rp_filter | reaches target |
|---|---|---|
| spoofed net-A source (10.0.1.10) | strict (1) | no (0) |
| real net-B source (10.0.2.10) | strict (1) | yes (3) |
| spoofed net-A source (10.0.1.10) | off (0) | yes (3) |
What You Will Learn
- What IP spoofing is and why it enables DDoS reflection and origin hiding.
- What ingress filtering (BCP 38) is and how reverse path filtering implements it.
- How the kernel does a reverse route lookup on the source address.
- The strict (1) vs loose (2) vs off (0)
rp_filtermodes. - Why strict rp_filter can misfire with asymmetric routing (and how that relates to Lab 38).
This lab does not cover:
- uRPF on hardware routers (the same idea in vendor gear).
- Full BCP 38 deployment at network edges.
- IPv6 rp_filter specifics.
Where to Read in the RFCs
| Reference | What to focus on |
|---|---|
| RFC 2827 (BCP 38) | The principle of ingress filtering |
| RFC 3704 (BCP 84) | Strict vs loose modes and multihoming |
| Linux ip-sysctl | rp_filter values 0/1/2; the max of all and per-interface |
| RFC 5737 / RFC 1918 | Confirming the addresses used here are local/documentation-only |
The Big Picture
The target (net A) and the attacker (net B) sit on opposite sides of router r. The attacker spoofs a net-A source address and aims at the target.
target (10.0.1.20) --- eth1 [ r ] eth2 --- attacker (10.0.2.10)
net A: 10.0.1.0/24 net B: 10.0.2.0/24
r's route to 10.0.1.0/24 is via eth1
flowchart LR
A["attacker (net B, eth2)<br/>src spoofed = 10.0.1.10"] -->|arrives on eth2| R["r<br/>reverse lookup: 10.0.1.10 → via eth1 ≠ eth2"]
R -->|"strict rp_filter: DROP ✘"| X[ ]
A2["attacker real src 10.0.2.10"] -->|arrives on eth2| R2["r<br/>10.0.2.10 → via eth2 ✔"]
R2 -->|"forwarded"| T["target"]
Note:
10.0.0.0/8is a local, closed range (RFC 1918 / RFC 5737), so nothing in this lab touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
nicolaka/netshoot:latest— bundlesnping,tcpdump, andsysctl.
No additional images are required.
Running the Lab
The quick path, which deploys, verifies, and tears down for you:
./scripts/labctl.sh run rpf-39
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/rpf-39
2. Deploy
sudo containerlab deploy -t rpf-39.clab.yml
3. Enable strict rp_filter
docker exec clab-rpf-39-r sh -c 'sysctl -w net.ipv4.conf.all.rp_filter=1; sysctl -w net.ipv4.conf.eth2.rp_filter=1'
4. Send spoofed packets and count them at the target
# capture on the target
docker exec -d clab-rpf-39-target sh -c "tcpdump -i eth1 -n 'icmp and src 10.0.1.10' -w /tmp/c.pcap"
# attacker spoofs a net-A source
docker exec clab-rpf-39-attacker nping --icmp -c 3 --source-ip 10.0.1.10 10.0.1.20
# count (0 with strict)
docker exec clab-rpf-39-target sh -c 'tcpdump -n -r /tmp/c.pcap | wc -l'
5. Confirm the real source still passes
docker exec -d clab-rpf-39-target sh -c "tcpdump -i eth1 -n 'icmp and src 10.0.2.10' -w /tmp/l.pcap"
docker exec clab-rpf-39-attacker nping --icmp -c 3 --source-ip 10.0.2.10 10.0.1.20
docker exec clab-rpf-39-target sh -c 'tcpdump -n -r /tmp/l.pcap | wc -l' # 3 (passes)
6. Turn rp_filter off and reproduce
docker exec clab-rpf-39-r sh -c 'sysctl -w net.ipv4.conf.all.rp_filter=0; sysctl -w net.ipv4.conf.eth2.rp_filter=0'
# the spoofed packets are now forwarded (3 arrive)
Expected Output
- strict (1): spoofed
10.0.1.10reaches the target 0 times (dropped at ingress). - strict (1): real
10.0.2.10reaches the target 3 times (only the forgery is blocked). - off (0): spoofed
10.0.1.10reaches the target 3 times (forwarded).
Why It Works
Reverse path filtering asks a single question: "would a reply to this source address go back out the interface the packet arrived on?"
- Spoofing. An attacker can forge the source address — IP itself never verifies it. Spoofing powers DDoS reflection (set the spoofed source to the victim, and responses converge on them), origin hiding, and impersonating trusted IPs.
- The reverse lookup. Normal forwarding looks up the destination. rp_filter additionally does a route lookup on the source, checking whether the best route back to that source matches the arrival interface. If it doesn't, that source shouldn't be coming from there — it's a forgery, so drop it.
- This lab's verdict. The attacker (net B, eth2) spoofs
10.0.1.10(net A).r's route to10.0.1.0/24is via eth1, but the packet arrived on eth2 — mismatch, so strict mode drops it. The attacker's real10.0.2.10legitimately arrives via eth2 — match, so it passes. rp_filter drops only the forgeries and never disturbs legitimate traffic. - The modes.
0is off;1is strict (the arrival interface must match the best return path);2is loose (any interface that can reach the source is fine). The effective value is the max ofconf.allandconf.<interface>. - Asymmetric routing caveat. In designs where forward and return traffic use different interfaces (some policy-routing setups, multihoming), strict mode can drop legitimate packets too. In those cases use loose (2), or design the paths to be symmetric.
The key insight: do a reverse lookup on the source at ingress, and drop any source that couldn't have come from there — that's the forgery. It's BCP 38 anti-spoofing, implemented in Linux.
Common Pitfalls
- Thinking rp_filter inspects the destination. It inspects the source (reverse lookup).
- Fearing it drops legitimate traffic. It only drops sources that "shouldn't arrive on that interface." Real traffic passes.
- Assuming strict is always right. It misfires under asymmetric routing. For multihoming, use loose or a symmetric design.
- Setting only
conf.all. The effective value is the max ofalland the per-interface setting — check both. - Equating it with a firewall. rp_filter checks source reachability; a stateful firewall (Lab 36) tracks connection state. Different layers.
- Expecting replies to spoofed packets to return. Replies to a spoofed source go to the address's real owner, not the attacker — which is exactly why spoofing is abused for reflection attacks.
Cleanup
sudo containerlab destroy -t rpf-39.clab.yml --cleanup
If you used labctl.sh run rpf-39, the script runs destroy for you at the end.
Check Your Understanding
- What is IP spoofing? Why is it dangerous (name one use)?
- What does reverse path filtering inspect in a packet, and how?
- In this lab, why is the spoofed
10.0.1.10dropped while the real10.0.2.10passes? - What is the difference between strict (1) and loose (2) rp_filter?
- Why does strict mode become a problem under asymmetric routing? How does that relate to policy routing (Lab 38)?
- How are the
conf.allandconf.<if>rp_filter values combined?
References
- RFC 2827 (BCP 38): Network Ingress Filtering
- RFC 3704 (BCP 84): Ingress Filtering for Multihomed Networks
- Linux ip-sysctl documentation (rp_filter)
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
Verified Run Log (2026-07-08)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
- Docker 29.1.3
- containerlab 0.77.0
- target / r / attacker:
nicolaka/netshoot:latest(nping,tcpdump,sysctl)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run rpf-39 performed deploy → verify → destroy, and verification.json returned "status": "verified".
Strict rp_filter drops only the forgeries
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.eth2.rp_filter = 1
spoofed_strict: 0 # spoofed 10.0.1.10 → 0 reach the target (dropped at ingress)
legit_strict: 3 # real 10.0.2.10 → 3 reach the target (passes)
spoofed_off: 3 # rp_filter=0 → spoofed 10.0.1.10 forwarded, 3 arrive
- The attacker (net B, eth2) spoofed net A's
10.0.1.10vianping --source-ip. Under strict mode,rreverse-looked-up the source: the route to10.0.1.0/24is via eth1, yet the packet arrived on eth2 — dropped at ingress, so 0 packets reached the target. - The same attacker sending from its real
10.0.2.10(correctly reachable via eth2) got 3 through — rp_filter stops only the forgery and never disturbs legitimate traffic. - With rp_filter set to 0, the same spoofed packets were forwarded and 3 arrived. This confirms that ingress filtering (BCP 38) blocks spoofing right at the entry point.
Cleanup
containerlab destroy -t rpf-39.clab.yml --cleanup
That's reverse path filtering: one extra route lookup on the source address at ingress, and forged packets never make it past the front door — the Linux take on BCP 38.
Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.
Next up, we'll keep building on the routing-and-security theme — see the repo for the latest labs.