Originally published at https://blog.pathvector.dev/protocol-lab-bgp-03/ — part of the free Protocol Lab series.
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.
In this lab, you will make two different ASNs announce the same prefix and observe how an upstream router sees the competing origins. The theme is simple but foundational: seeing a prefix in BGP is not the same as knowing who is allowed to originate it.
Reading guide: rfc-notes/bgp-rfc4271-lab03.md
Expected time: 45–60 minutes.
The Goal
Two ASes originate 203.0.113.0/24, and the observer router r2 ends up holding two paths for the same prefix. By the end, you should be able to explain this observation:
203.0.113.0/24 has two BGP paths on r2
path via 10.0.12.1 has origin AS65001
path via 10.0.23.2 has origin AS65003
You will read the rightmost AS in each AS_PATH as the origin AS, and explain why a competing origin — the same prefix visible from multiple origin ASes — is the doorway to some of BGP's most notorious failure modes.
What You Will Learn
- The rightmost AS in the
AS_PATHis read as the route's origin AS. - The same prefix can be visible from multiple origin ASes at once.
- Competing origins can happen for several reasons: misconfiguration, intentional multihoming, route leaks, or hijacks.
- The BGP table alone cannot tell you whether an origin AS is actually authorized.
- RPKI origin validation — the subject of the next lab — is the tool that answers that question.
This lab does not cover:
- RPKI / ROA / ROV
- Route-leak taxonomy in detail
- The BGP decision process in detail
- AS-path validation
- Announcements to the public internet
Where to Read in the RFCs
The required reading this time is RFC 4271, in these sections:
| Section | What to focus on |
|---|---|
| 1.1 | Terminology: AS, BGP speaker, route, RIB |
| 3.1 | A route is a pairing of a prefix and its path attributes |
| 4.3 | UPDATE messages carry path attributes and NLRI |
| 5.1.2 |
AS_PATH and AS_SEQUENCE
|
| 9 | How a BGP speaker processes received UPDATEs, storing and selecting routes |
As supporting material, read the problem statement in RFC 7908 as your entry point into route leaks.
The Big Picture
We build three virtual routers.
AS65001 / r1 AS65002 / r2 AS65003 / r3
10.0.12.1/30 ---------------------- 10.0.12.2/30
10.0.23.1/30 ---------------------- 10.0.23.2/30
r1 advertises:
203.0.113.0/24
r3 also advertises:
203.0.113.0/24
r2 observes:
203.0.113.0/24 via 10.0.12.1, AS_PATH 65001
203.0.113.0/24 via 10.0.23.2, AS_PATH 65003
203.0.113.0/24 is an RFC 5737 documentation prefix. It is never advertised externally — it lives only inside this lab.
flowchart LR
r1["r1<br/>AS65001<br/>originates 203.0.113.0/24"]
r2["r2<br/>AS65002<br/>observer"]
r3["r3<br/>AS65003<br/>also originates 203.0.113.0/24"]
r1 -- "eBGP<br/>10.0.12.0/30" --> r2
r3 -- "eBGP<br/>10.0.23.0/30" --> r2
flowchart TB
prefix["203.0.113.0/24"]
path1["Path 1<br/>NEXT_HOP 10.0.12.1<br/>AS_PATH 65001<br/>origin AS65001"]
path2["Path 2<br/>NEXT_HOP 10.0.23.2<br/>AS_PATH 65003<br/>origin AS65003"]
question["Question<br/>Which origin is authorized?"]
prefix --> path1
prefix --> path2
path1 --> question
path2 --> question
Note: This lab runs entirely inside a closed Docker environment using an RFC 5737 documentation prefix. Never advertise documentation prefixes to the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
-
tcpdumpon the host - Wireshark or tshark
Image used:
frrouting/frr:latest
On macOS, run this inside a Linux VM, a WSL-equivalent environment, or a Linux VM on OrbStack/Colima.
Running the Lab
All steps below happen inside the Linux environment where containerlab runs.
If you have the repo checked out, the verification script does everything in one go:
./scripts/labctl.sh run bgp-03
labctl.sh run bgp-03 handles the topology deploy, FRR output checks, pcap capture, and destroy.
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/bgp-03
2. Deploy
sudo containerlab deploy -t bgp-03.clab.yml
After the deploy, confirm the containers are up:
docker ps --format "table {{.Names}}\t{{.Status}}"
What to check:
-
clab-bgp-03-r1is running. -
clab-bgp-03-r2is running. -
clab-bgp-03-r3is running.
Give the neighbors a few seconds to reach Established, then look at the BGP summary:
docker exec -it clab-bgp-03-r2 vtysh -c "show bgp summary"
What to observe:
-
r2's local AS is65002. - Neighbor
10.0.12.1is AS65001. - Neighbor
10.0.23.2is AS65003. - A prefix has been received from both neighbors.
3. See the two paths for the same prefix on r2
docker exec -it clab-bgp-03-r2 vtysh -c "show bgp ipv4 unicast 203.0.113.0/24"
How to read the output:
Paths: (2 available, best #...)
65001
10.0.12.1 from 10.0.12.1 ...
Origin IGP ...
65003
10.0.23.2 from 10.0.23.2 ...
Origin IGP ...
The ordering can vary with the FRRouting version and how the best path is chosen. What matters is that the same 203.0.113.0/24 shows two origin ASes: 65001 and 65003.
4. See both UPDATEs in a packet capture
Open another terminal and, still inside the Linux environment running containerlab, capture in r2's namespace:
sudo ip netns exec clab-bgp-03-r2 tcpdump -i any -nn -s 0 -w bgp-03-r2.pcap tcp port 179
With the capture running, bounce r2's BGP sessions from another terminal:
docker exec -it clab-bgp-03-r2 vtysh -c "clear bgp *"
A few seconds later, stop tcpdump with Ctrl-C. bgp-03-r2.pcap is left in your current working directory.
Open it in Wireshark and look for:
- The BGP UPDATE from
10.0.12.1 -> 10.0.12.2AS_PATH: 65001NLRI: 203.0.113.0/24
- The BGP UPDATE from
10.0.23.2 -> 10.0.23.1AS_PATH: 65003NLRI: 203.0.113.0/24
The point:
- The prefix is the same.
- The origin AS is different.
- Nothing in the BGP UPDATE itself tells you which origin is the authorized one.
Expected Output
Rather than an exact character-for-character match, focus on being able to pull out these fields.
show bgp summary
On r2, confirm:
Local AS number 65002
Neighbor V AS ... State/PfxRcd
10.0.12.1 4 65001 ... 1
10.0.23.2 4 65003 ... 1
show bgp ipv4 unicast 203.0.113.0/24
On r2, confirm:
Paths: (2 available, best #...)
65001
10.0.12.1 from 10.0.12.1 ...
Origin IGP ...
65003
10.0.23.2 from 10.0.23.2 ...
Origin IGP ...
What to look for:
-
203.0.113.0/24is visible via two paths. - One
AS_PATHis65001. - The other
AS_PATHis65003. - The rightmost AS in each
AS_PATHreads as the origin AS.
Why It Works
r1 originates 203.0.113.0/24 as AS65001. r3 also originates the same 203.0.113.0/24 — but as AS65003.
r2 receives UPDATEs carrying the same NLRI from two eBGP neighbors. Each UPDATE carries its own path attributes, and the AS_PATH differs between them.
At this point, r2 is looking at two claims of reachability for the same prefix. BGP may pick a single best path, but the important observation is that multiple origin ASes are visible for the same prefix.
This is not always an attack. Intentional multihoming or a migration in progress can legitimately produce multiple origins. But it is also the entry point for misadvertisements, route leaks, and prefix hijacks. And the BGP table alone gives you no way to judge whether an origin AS is authorized.
Common Misconceptions
- A competing origin does not automatically mean a hijack.
- Route leaks and competing origins are not synonyms. A route leak is about the propagation scope and business relationships of an advertisement; a competing origin is the state where multiple origin ASes are visible for the same prefix.
- Paths that lose the best-path decision still matter for observation.
- The
AS_PATHis not proof of prefix ownership. - This lab runs only inside a closed Docker environment. Never advertise documentation prefixes to the real internet.
Troubleshooting
r2 only sees one path
Check that both neighbors are Established:
docker exec -it clab-bgp-03-r2 vtysh -c "show bgp summary"
If you don't see both 10.0.12.1 and 10.0.23.2, verify the interface addresses:
docker exec -it clab-bgp-03-r1 ip addr show eth1
docker exec -it clab-bgp-03-r2 ip addr show eth1
docker exec -it clab-bgp-03-r2 ip addr show eth2
docker exec -it clab-bgp-03-r3 ip addr show eth1
r1 or r3 isn't advertising the prefix
Check each router's loopback and FRR config:
docker exec -it clab-bgp-03-r1 ip addr show lo
docker exec -it clab-bgp-03-r3 ip addr show lo
docker exec -it clab-bgp-03-r1 vtysh -c "show running-config"
docker exec -it clab-bgp-03-r3 vtysh -c "show running-config"
FRRouting's network 203.0.113.0/24 only advertises the prefix when a matching route exists in the routing table.
Only one UPDATE shows up in the pcap
Bounce the BGP sessions after the capture has started:
docker exec -it clab-bgp-03-r2 vtysh -c "clear bgp *"
If you still only see one, confirm you're capturing with -i any inside the r2 namespace.
Check Your Understanding
- What are the two
AS_PATHs thatr2sees for203.0.113.0/24? - What is the origin AS of each?
- Why is seeing multiple origins for the same prefix a doorway to danger?
- What is the difference between a competing origin and a route leak?
- Explain why the BGP table alone cannot tell you whether an origin is authorized.
Cleanup
When you're done, destroy the containerlab topology:
sudo containerlab destroy -t bgp-03.clab.yml
Confirm no containers are left behind:
docker ps --format "table {{.Names}}\t{{.Status}}" | grep clab-bgp-03 || true
If you want to keep the pcap, move it to assets/bgp-03/:
mkdir -p ../../assets/bgp-03
cp bgp-03-r2.pcap ../../assets/bgp-03/
Otherwise, delete it:
rm -f bgp-03-r2.pcap
What to Read Next
The next lab compares RPKI ROAs / VRPs against BGP routes and shows why an origin AS comes out as valid, invalid, or not-found.
Read next:
- RFC 6482: Route Origin Authorization
- RFC 6811: BGP Prefix Origin Validation
- RFC 8210: RPKI to Router Protocol
References
- RFC 4271, Section 1.1: Definition of common BGP terms
- RFC 4271, Section 3.1: Routes, advertisement, and storage
- RFC 4271, Section 4.3: UPDATE Message Format
- RFC 4271, Section 5.1.2: AS_PATH
- RFC 4271, Section 9: UPDATE Message Handling
- RFC 5737, Section 3: Documentation address blocks, including
203.0.113.0/24 - RFC 7908: Problem Definition and Classification of BGP Route Leaks
Verified Run Log (2026-05-09)
This lab has been confirmed reproducible. The log below is based on assets/bgp-03/runs/20260509T060606Z/run.log.
verification.json reported:
{
"lab": "bgp-03",
"run_id": "20260509T060606Z",
"status": "verified",
"message": "BGP route 203.0.113.0/24 is visible on r2 from origin AS65001 and AS65003."
}
On r2 — the observation point of the three-router topology — each of the two eBGP neighbors advertised one prefix:
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
10.0.12.1 4 65001 4 4 0 0 0 00:00:01 1 1 N/A
10.0.23.2 4 65003 4 4 0 0 0 00:00:01 1 1 N/A
For the same 203.0.113.0/24, r2's BGP table showed two paths:
Network Next Hop Metric LocPrf Weight Path
*> 203.0.113.0/24 10.0.12.1 0 0 65001 i
* 10.0.23.2 0 0 65003 i
Displayed 1 routes and 2 total paths
Comparing the two paths, the rightmost origin AS differs:
| Next hop | AS_PATH | Origin AS | BGP table status |
|---|---|---|---|
10.0.12.1 |
65001 |
65001 |
valid, best |
10.0.23.2 |
65003 |
65003 |
valid |
In the detailed view, FRRouting selected the 65001 side as the best path:
BGP routing table entry for 203.0.113.0/24, version 1
Paths: (2 available, best #1, table default)
Advertised to non peer-group peers:
10.0.12.1 10.0.23.2
65001
10.0.12.1 from 10.0.12.1 (1.1.1.1)
Origin IGP, metric 0, valid, external, best (Router ID)
Last update: Sat May 9 06:06:09 2026
65003
10.0.23.2 from 10.0.23.2 (3.3.3.3)
Origin IGP, metric 0, valid, external
Last update: Sat May 9 06:06:09 2026
In this run, 203.0.113.0/24 was visible from both AS65001 and AS65003, and r2 chose the AS65001 path as best. But a best path in the BGP table is not proof of an authorized origin. To judge which origin AS is legitimate, you need the ROAs / VRPs and origin validation covered in the next lab.
That's the competing-origins problem in a nutshell: BGP faithfully carries every claim of "I can reach this prefix," and nothing in the protocol says which claim is true.
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 bring in RPKI: matching ROAs against BGP routes to see exactly why an origin comes out valid, invalid, or not-found.
Top comments (0)