RPKI Origin Validation: Watch BGP Routes Turn Valid, Invalid, and Not Found
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.
BGP will happily show you who originated a route — but it can't tell you whether that origin was allowed to. That's the gap RPKI origin validation fills. In this lab, you'll connect FRRouting to a local RPKI-to-Router (RTR) cache and observe all three origin validation states side by side:
valid: the route's origin matches a VRP.invalid: the prefix is covered by a VRP, but the origin AS is not authorized.not found: no VRP covers the route.
Reading guide: rfc-notes/rpki-origin-validation.md
Expected time: 60–75 minutes.
The Goal
By the end, you should be able to explain this table:
| Route observed on r2 | Origin AS | Local VRP says | State |
|---|---|---|---|
203.0.113.0/24 via 10.0.12.1 |
65001 |
203.0.113.0/24, max length 24, AS 65001 |
valid |
203.0.113.0/24 via 10.0.23.2 |
65003 |
same prefix, but AS 65001 only |
invalid |
198.51.100.0/24 via 10.0.24.2 |
65004 |
no covering VRP | not found |
The theme is simple: BGP can show who originated a route, but RPKI origin validation helps answer whether that origin is authorized. This lab is the entry point for cross-checking the origin AS you see in BGP against the authorization data RPKI provides.
What You Will Learn
- A ROA authorizes an AS to originate an IP prefix up to a maximum prefix length.
- A router usually receives validated prefix data as VRPs from an RTR cache.
- Origin validation compares a BGP route's prefix and origin AS against VRPs.
valid,invalid, andnot foundare different states.- RPKI origin validation checks the origin AS, not the entire AS_PATH.
- A router does not necessarily reject
invalidroutes unless policy tells it to do so.
This lab does not cover:
- real RPKI repository synchronization
- public RTR cache operation
- route filtering policy
- ASPA or full AS_PATH validation
- production RPKI operations
Where to Read in the RFCs
The must-reads for this lab:
| RFC | Section | What to focus on |
|---|---|---|
| RFC 6482 | 3 | A ROA binds an AS to an IP prefix |
| RFC 6811 | 2 | The terms route, origin AS, VRP, and covering prefix |
| RFC 6811 | 2.1 | How valid / invalid / not found are determined |
| RFC 8210 | 1–2 | The validated cache and router exchange data over the RTR protocol |
| RFC 5737 | 3 | 203.0.113.0/24 and 198.51.100.0/24 are documentation prefixes |
The Big Picture
We build four FRRouting routers and one local StayRTR cache.
AS65001 / r1 ----\
\
AS65002 / r2 ---- StayRTR local RTR cache
/
AS65003 / r3 ----/
AS65004 / r4 ----/
r1 advertises:
203.0.113.0/24 -> expected valid
r3 advertises:
203.0.113.0/24 -> expected invalid
r4 advertises:
198.51.100.0/24 -> expected not found
local VRP data:
203.0.113.0/24, maxLength 24, AS65001
203.0.113.0/24 and 198.51.100.0/24 are RFC 5737 documentation prefixes. They are never advertised externally and are used only inside the lab.
Note: Everything here uses documentation address space (RFC 5737), so nothing in this lab touches the real internet.
flowchart LR
r1["r1<br/>AS65001<br/>originates 203.0.113.0/24"]
r2["r2<br/>AS65002<br/>BGP observer<br/>RPKI client"]
r3["r3<br/>AS65003<br/>originates 203.0.113.0/24"]
r4["r4<br/>AS65004<br/>originates 198.51.100.0/24"]
stayrtr["StayRTR<br/>local RTR cache<br/>VRP: 203.0.113.0/24 maxLength 24 AS65001"]
r1 -- "eBGP<br/>10.0.12.0/30" --> r2
r3 -- "eBGP<br/>10.0.23.0/30" --> r2
r4 -- "eBGP<br/>10.0.24.0/30" --> r2
r2 -- "RTR<br/>10.0.25.0/30<br/>tcp/8282" --> stayrtr
And here's the decision logic origin validation applies to every route:
flowchart TB
route["BGP route<br/>prefix + origin AS"]
vrp["VRP<br/>prefix + max length + AS"]
compare["Origin validation<br/>compare route against VRPs"]
valid["valid<br/>prefix covered and origin AS matches"]
invalid["invalid<br/>prefix covered but origin AS mismatches"]
notfound["not found<br/>no covering VRP"]
route --> compare
vrp --> compare
compare --> valid
compare --> invalid
compare --> notfound
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
frrouting/frr:latest— the four BGP routersrpki/stayrtr:latest— the local RTR cache
On macOS, run this inside a Linux VM, a WSL-equivalent environment, or a Linux VM on OrbStack/Colima.
Running the Lab
All the steps below run inside the Linux environment where containerlab is installed.
If you have the repo checked out, the quick path deploys, verifies FRRouting output, checks the RPKI validation states, and tears everything down for you:
./scripts/labctl.sh run rpki-04
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/rpki-04
2. Read the local VRP data
In this lab, StayRTR gets its VRPs from a local JSON file rather than a real RPKI repository.
cat stayrtr/roas.json
Expected content:
{
"roas": [
{
"prefix": "203.0.113.0/24",
"maxLength": 24,
"asn": 65001
}
]
}
How to read it:
203.0.113.0/24may be originated by AS65001.maxLengthis24, so more-specific prefixes are not authorized.- AS65003 is not an authorized origin for this prefix.
- There is no VRP for
198.51.100.0/24at all.
3. Deploy
sudo containerlab deploy -t rpki-04.clab.yml
After it comes up, confirm the containers exist:
docker ps --format "table {{.Names}}\t{{.Status}}"
Checkpoints:
clab-rpki-04-r1is running.clab-rpki-04-r2is running.clab-rpki-04-r3is running.clab-rpki-04-r4is running.clab-rpki-04-stayrtris running.
4. Confirm r2 is connected to the RTR cache
docker exec -it clab-rpki-04-r2 vtysh -c "show rpki cache-connection"
Checkpoint:
Connected to group 1
rpki tcp cache 10.0.25.2 8282 pref 1 (connected)
Next, look at the VRPs r2 has received:
docker exec -it clab-rpki-04-r2 vtysh -c "show rpki prefix-table"
Checkpoint:
RPKI/RTR prefix table
Prefix Prefix Length Origin-AS
203.0.113.0 24 - 24 65001
5. See the validation states in the BGP table
docker exec -it clab-rpki-04-r2 vtysh -c "show bgp ipv4 unicast"
Checkpoint:
RPKI validation codes: V valid, I invalid, N Not found
Network Next Hop Metric LocPrf Weight Path
N*> 198.51.100.0/24 10.0.24.2 0 0 65004 i
I* 203.0.113.0/24 10.0.23.2 0 0 65003 i
V*> 10.0.12.1 0 0 65001 i
When FRRouting shows multiple paths for the same prefix, it may leave the Network column blank on the second and later lines. In the example above, the V*> line is also a path for 203.0.113.0/24.
How to read it:
Visvalid:203.0.113.0/24originated by AS65001 matches the VRP.Iisinvalid:203.0.113.0/24originated by AS65003, but the VRP only authorizes AS65001.Nisnot found: there is no VRP covering198.51.100.0/24.
You can also filter the view by state:
docker exec -it clab-rpki-04-r2 vtysh -c "show bgp ipv4 unicast rpki valid"
docker exec -it clab-rpki-04-r2 vtysh -c "show bgp ipv4 unicast rpki invalid"
Expected Output
Rather than an exact character-for-character match, focus on capturing these fields.
show rpki cache-connection
rpki tcp cache 10.0.25.2 8282 pref 1 (connected)
What to look for:
r2is connected to the local StayRTR cache.- The RTR cache is at
10.0.25.2:8282.
show rpki prefix-table
203.0.113.0 24 - 24 65001
What to look for:
- The VRP covers
203.0.113.0/24. - The max length is
24. - The authorized origin AS is
65001.
show bgp ipv4 unicast
What to look for:
203.0.113.0/24from AS65001 isV.203.0.113.0/24from AS65003 isI.198.51.100.0/24from AS65004 isN.
Why It Works
RPKI origin validation compares each BGP route's prefix and origin AS against the VRPs obtained from a validated cache.
This lab has exactly one local VRP:
203.0.113.0/24, maxLength 24, AS65001
r1's route matches the VRP on both prefix and origin AS, so it'svalid.r3's route has a prefix that is covered by the VRP, but its origin AS is65003, so it'sinvalid.r4's route has no covering VRP for198.51.100.0/24, so it'snot found.
One thing to keep firmly in mind: a route being labeled invalid and a route being rejected are two different things. In FRRouting, unless you write policy, an invalid route can still become the best path. This lab stops at observing the validation states; filter policy is a separate topic.
Common Pitfalls
- A "blank"
Networkcolumn inshow bgp ipv4 unicast. That's just FRRouting omitting the prefix when showing multiple paths for the same prefix — the line still belongs to the prefix above it. - Treating
not foundasinvalid. They're different:not foundmeans no covering VRP exists at all. - Reading
invalidas "the whole AS_PATH is forged." It only means the origin AS doesn't match the VRP. - Expecting origin validation to filter routes by itself. It's not a route filtering policy — rejecting
invalidroutes requires separate policy. - Confusing this lab's JSON with real RPKI. The JSON here is local experiment data. In the real RPKI, ROAs are signed objects validated from repositories.
Cleanup
If you deployed manually, destroy the topology:
sudo containerlab destroy -t rpki-04.clab.yml --cleanup
If you used labctl.sh run rpki-04, the script runs destroy for you at the end.
Check Your Understanding
- Why is
203.0.113.0/24from AS65001valid? - Why is
203.0.113.0/24from AS65003invalid? - Why is
198.51.100.0/24from AS65004not found? - Are
invalidroutes always rejected automatically? - Does RPKI origin validation verify the entire AS_PATH?
References
- RFC 6482: A Profile for Route Origin Authorizations (ROAs)
- RFC 6811: BGP Prefix Origin Validation
- RFC 8210: The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 1
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
- FRRouting BGP documentation
- StayRTR
That's RPKI origin validation in one sitting: one VRP, three routes, three states — and the crucial realization that invalid is a label, not a verdict, until policy says otherwise.
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 look at turning validation states into action — writing route policy that actually rejects invalid routes.