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_filter modes.
  • 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/8 is 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 — bundles nping, tcpdump, and sysctl.

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.10 reaches the target 0 times (dropped at ingress).
  • strict (1): real 10.0.2.10 reaches the target 3 times (only the forgery is blocked).
  • off (0): spoofed 10.0.1.10 reaches 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 to 10.0.1.0/24 is via eth1, but the packet arrived on eth2 — mismatch, so strict mode drops it. The attacker's real 10.0.2.10 legitimately arrives via eth2 — match, so it passes. rp_filter drops only the forgeries and never disturbs legitimate traffic.
  • The modes. 0 is off; 1 is strict (the arrival interface must match the best return path); 2 is loose (any interface that can reach the source is fine). The effective value is the max of conf.all and conf.<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 of all and 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

  1. What is IP spoofing? Why is it dangerous (name one use)?
  2. What does reverse path filtering inspect in a packet, and how?
  3. In this lab, why is the spoofed 10.0.1.10 dropped while the real 10.0.2.10 passes?
  4. What is the difference between strict (1) and loose (2) rp_filter?
  5. Why does strict mode become a problem under asymmetric routing? How does that relate to policy routing (Lab 38)?
  6. How are the conf.all and conf.<if> rp_filter values combined?

References

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.10 via nping --source-ip. Under strict mode, r reverse-looked-up the source: the route to 10.0.1.0/24 is 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.

Read more