BGP From Scratch: Announce One Prefix and Explain Every Field of the Route
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 first BGP lab, you will build a tiny eBGP session between two ASNs, advertise one prefix, and explain the route the other side learns. The theme is deliberately simple: one prefix announcement, one route you can fully explain.
By the end, you should be able to read this single route in RFC 4271 terms:
203.0.113.0/24 via 10.0.0.1, AS_PATH 65001, ORIGIN IGP
Reading guide: rfc-notes/bgp-rfc4271.md
Expected time: 45–60 minutes.
What You Will Learn
- An AS is the administrative unit of BGP's external route exchange.
- A prefix is a reachable range of destination addresses.
- A BGP route is
prefix + path attributes. - Route advertisements are carried in UPDATE messages.
- On the receiving side, you read the NLRI,
AS_PATH,NEXT_HOP, andORIGINtogether as one route.
This lab does not cover:
- The details of the best-path selection algorithm
- Route withdrawal
- RPKI / ROA / ROV
- iBGP
- Route reflectors
- Announcing anything to the real 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 | AS, BGP speaker, EBGP, IBGP, NLRI, Route |
| 3 | BGP exchanges network reachability information |
| 3.1 | A route is a prefix plus path attributes, advertised in UPDATE messages |
| 4.2 | The OPEN message carries My Autonomous System |
| 4.3 | UPDATE message structure — look at Path Attributes and NLRI |
| 5 | ORIGIN, AS_PATH, and NEXT_HOP are mandatory attributes |
| 5.1.1 | ORIGIN |
| 5.1.2 | AS_PATH |
| 5.1.3 | NEXT_HOP |
The Big Picture
We build two virtual routers:
AS65001 / r1 AS65002 / r2
10.0.0.1/30 ---------------------- 10.0.0.2/30
r1 advertises:
203.0.113.0/24
r2 learns:
prefix: 203.0.113.0/24
next hop: 10.0.0.1
as path: 65001
origin: IGP
203.0.113.0/24 is an RFC 5737 documentation prefix. It is never advertised externally — it lives only inside the lab.
flowchart LR
r1["r1<br/>AS65001<br/>10.0.0.1/30<br/>originates 203.0.113.0/24"]
r2["r2<br/>AS65002<br/>10.0.0.2/30<br/>learns 203.0.113.0/24"]
r1 -- "eBGP session<br/>TCP/179" --> r2
What to notice in this diagram:
r1andr2are in different ASes, so this session is eBGP.r1originates203.0.113.0/24.r2receives the UPDATE and installs a route to203.0.113.0/24in its BGP table.
Note: This lab uses a documentation prefix (RFC 5737) inside a closed Docker environment, so nothing here touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
tcpdumpon the host- Wireshark
Images used:
frrouting/frr:latest
On macOS, run everything inside a Linux VM, a WSL-equivalent environment, or a Linux VM via OrbStack/Colima. BGP packet capture is easier to work with as teaching material when done inside Linux namespaces.
Running the Lab
All the steps below run inside the Linux environment where containerlab lives.
If you are reading this on macOS, do not run containerlab directly from the macOS terminal — enter your Linux VM / OrbStack / Colima environment first. Do the packet capture in that same Linux environment too, then open the resulting pcap in Wireshark on the macOS side if you like.
If you have the repo, the verification script runs the whole thing for you:
./scripts/labctl.sh run bgp-01
labctl.sh run bgp-01 deploys the topology, checks the FRR output, captures the pcap, and destroys the lab.
Or step through it manually:
1. Create the working directory
If you have the repo, use the sample configs as-is:
cd protocol-lab/examples/bgp-01
If you are only reading the article, create an empty working directory and build the files below:
mkdir -p bgp-01
cd bgp-01
2. Create the containerlab topology
bgp-01.clab.yml:
name: bgp-01
topology:
nodes:
r1:
kind: linux
image: frrouting/frr:latest
binds:
- ./r1/frr.conf:/etc/frr/frr.conf
- ./r1/vtysh.conf:/etc/frr/vtysh.conf
- ./r1/daemons:/etc/frr/daemons
exec:
- ip addr add 10.0.0.1/30 dev eth1
- ip link set eth1 up
- ip addr add 203.0.113.1/24 dev lo
- sysctl -w net.ipv4.ip_forward=1
r2:
kind: linux
image: frrouting/frr:latest
binds:
- ./r2/frr.conf:/etc/frr/frr.conf
- ./r2/vtysh.conf:/etc/frr/vtysh.conf
- ./r2/daemons:/etc/frr/daemons
exec:
- ip addr add 10.0.0.2/30 dev eth1
- ip link set eth1 up
- sysctl -w net.ipv4.ip_forward=1
links:
- endpoints: ["r1:eth1", "r2:eth1"]
3. Create the FRRouting configs
mkdir -p r1 r2
r1/daemons:
zebra=yes
bgpd=yes
r1/vtysh.conf:
service integrated-vtysh-config
r2/daemons:
zebra=yes
bgpd=yes
r2/vtysh.conf:
service integrated-vtysh-config
r1/frr.conf:
frr version 10.0
frr defaults traditional
hostname r1
service integrated-vtysh-config
!
router bgp 65001
bgp router-id 1.1.1.1
no bgp ebgp-requires-policy
neighbor 10.0.0.2 remote-as 65002
!
address-family ipv4 unicast
network 203.0.113.0/24
exit-address-family
!
line vty
r2/frr.conf:
frr version 10.0
frr defaults traditional
hostname r2
service integrated-vtysh-config
!
router bgp 65002
bgp router-id 2.2.2.2
no bgp ebgp-requires-policy
neighbor 10.0.0.1 remote-as 65001
!
address-family ipv4 unicast
exit-address-family
!
line vty
4. Deploy
sudo containerlab deploy -t bgp-01.clab.yml
Once it comes up, confirm the containers exist:
docker ps --format "table {{.Names}}\t{{.Status}}"
What to check:
clab-bgp-01-r1is running.clab-bgp-01-r2is running.
Give the neighbors a few seconds to reach Established, then look at the BGP summary:
docker exec -it clab-bgp-01-r1 vtysh -c "show bgp summary"
docker exec -it clab-bgp-01-r2 vtysh -c "show bgp summary"
What to observe:
r1's AS is65001.r2's AS is65002.- The peer reaches an
Established-equivalent state. - On
r2,PfxRcd(orState/PfxRcd) shows1. - On
r1, a received-prefix count of0is fine —r2isn't advertising any prefix in this lab.
5. Look at the route r2 received
docker exec -it clab-bgp-01-r2 vtysh -c "show bgp ipv4 unicast"
How to read the expected output:
Network Next Hop Path
*> 203.0.113.0/24 10.0.0.1 65001 i
The exact formatting varies a little between FRRouting versions, but the columns to look at are the same:
| Display | RFC 4271 equivalent |
|---|---|
203.0.113.0/24 |
NLRI / prefix |
10.0.0.1 |
NEXT_HOP |
65001 |
AS_PATH |
i |
ORIGIN = IGP |
flowchart TB
route["BGP route on r2"]
nlri["NLRI / prefix<br/>203.0.113.0/24"]
nexthop["NEXT_HOP<br/>10.0.0.1"]
aspath["AS_PATH<br/>65001"]
origin["ORIGIN<br/>IGP"]
nlri --> route
nexthop --> route
aspath --> route
origin --> route
That one line of show bgp ipv4 unicast is not just terminal output — it's a small, live observation of RFC 4271's route = prefix + path attributes.
Look at the detailed view too:
docker exec -it clab-bgp-01-r2 vtysh -c "show bgp ipv4 unicast 203.0.113.0/24"
What to observe:
65001appears underPaths.- You see
from 10.0.0.1ornexthop 10.0.0.1. - You see
origin IGP. - This one entry corresponds to RFC 4271's
route = prefix + path attributes.
6. Watch the UPDATE in a packet capture
Open another terminal — still inside the Linux environment running containerlab — and capture on r2's interface.
The FRRouting container doesn't necessarily ship with tcpdump, so capture from the host by entering the network namespace containerlab created.
First, confirm the namespaces are visible:
sudo ip netns list | grep clab-bgp-01
What to check:
clab-bgp-01-r1is listed.clab-bgp-01-r2is listed.
Start the capture:
sudo ip netns exec clab-bgp-01-r2 tcpdump -i eth1 -nn -s 0 -w bgp-01-r2.pcap tcp port 179
With the capture running, re-establish the BGP session from r1:
docker exec -it clab-bgp-01-r1 vtysh -c "clear bgp 10.0.0.2"
After a few seconds, stop tcpdump with Ctrl-C. bgp-01-r2.pcap is left in your current working directory.
Open it in Wireshark and look for:
BGP OPENMy AS: 65001BGP Identifier: 1.1.1.1
BGP UPDATEPath attributesORIGINAS_PATH: 65001NEXT_HOP: 10.0.0.1NLRI: 203.0.113.0/24
If you want to open it in Wireshark on macOS, move the generated bgp-01-r2.pcap over via a shared directory or scp first.
Expected Output
Rather than an exact character-for-character match, the goal is being able to pull out the following fields.
show bgp summary
On r2:
Local AS number 65002
Neighbor V AS MsgRcvd MsgSent ... State/PfxRcd
10.0.0.1 4 65001 ... ... ... 1
What to look for:
- The local AS is
65002. - The neighbor is
10.0.0.1. - The neighbor AS is
65001. State/PfxRcdis1, or the output otherwise shows an Established session with one prefix received.
show bgp ipv4 unicast
On r2:
Network Next Hop Path
*> 203.0.113.0/24 10.0.0.1 65001 i
What to look for:
203.0.113.0/24is in the BGP table.NEXT_HOPis10.0.0.1.AS_PATHis65001.ORIGINisi(orIGPin the detailed view).
show bgp ipv4 unicast 203.0.113.0/24
In the detailed view:
Paths: (1 available, best #1, table default)
65001
10.0.0.1 from 10.0.0.1 ...
Origin IGP ...
What to look for:
65001appears as the path.- You see
from 10.0.0.1ornexthop 10.0.0.1. - You see
Origin IGP.
Why It Works
r1 has network 203.0.113.0/24 configured inside router bgp 65001. On top of that, its lo interface carries 203.0.113.1/24, so FRRouting can originate that prefix into BGP.
r1 then sends an UPDATE to its external peer, r2. Under RFC 4271 Section 5.1.2, when the originating speaker sends a route to an external peer, it puts its own AS number into the AS_PATH. That's why the AS_PATH seen from r2 is 65001.
r2 learns: to reach 203.0.113.0/24, send to 10.0.0.1. That's the NEXT_HOP.
Common Misconceptions
- The prefix in the
Networkcolumn is not a next hop. It's the destination range you want to reach. Next Hopis not the origin AS. It's the IP address packets are forwarded to next.AS_PATHis not a list of physical router names. It's a sequence of AS numbers.- The
idoes not mean iBGP. It's the display for the ORIGIN attribute'sIGPvalue. - A route appearing in the BGP table does not mean it was advertised to the real internet. This lab runs entirely inside a closed Docker environment.
Common Pitfalls
show bgp summary never reaches Established
Check the basics:
docker exec -it clab-bgp-01-r1 ip addr show eth1
docker exec -it clab-bgp-01-r2 ip addr show eth1
docker exec -it clab-bgp-01-r1 ping -c 3 10.0.0.2
docker exec -it clab-bgp-01-r2 ping -c 3 10.0.0.1
If eth1 is missing 10.0.0.1/30 or 10.0.0.2/30, containerlab's exec steps may not have run as expected.
203.0.113.0/24 doesn't show up on r2
Check whether r1 actually holds that prefix locally:
docker exec -it clab-bgp-01-r1 ip addr show lo
docker exec -it clab-bgp-01-r1 vtysh -c "show running-config"
FRRouting's network 203.0.113.0/24 only advertises the prefix when a matching prefix exists in the routing table. In this topology, that condition is satisfied by putting 203.0.113.1/24 on r1's loopback.
tcpdump can't find the namespace
In the Linux environment running containerlab, check:
sudo ip netns list
If you're running this from a macOS terminal, Linux network namespaces are not visible. Run it inside the Linux VM / OrbStack / Colima environment.
No UPDATE in the pcap
If you start the capture after the BGP session is already Established, you can miss the initial UPDATE. Start the capture first, then re-establish the session:
docker exec -it clab-bgp-01-r1 vtysh -c "clear bgp 10.0.0.2"
If you still don't see it, double-check which interface tcpdump is watching:
sudo ip netns exec clab-bgp-01-r2 ip link
Check Your Understanding
- Looking at
show bgp ipv4 unicastonr2, which parts of the203.0.113.0/24entry are the NLRI,AS_PATH,NEXT_HOP, andORIGIN? - If you changed
r1's AS number to65010, how would theAS_PATHseen fromr2change? - If you removed
network 203.0.113.0/24fromr1and reloaded FRR, how wouldr2's BGP table change? Which RFC 4271 concept does that connect to? - Explain in your own words why the
NEXT_HOPis10.0.0.1. - What does
ORIGIN = IGPactually mean by "IGP"? Does it mean the route was learned via OSPF?
Cleanup
When you're done, destroy the containerlab topology:
sudo containerlab destroy -t bgp-01.clab.yml
Confirm no containers are left behind:
docker ps --format "table {{.Names}}\t{{.Status}}" | grep clab-bgp-01 || true
If you want to keep the pcap, move it into assets/bgp-01/:
mkdir -p ../assets/bgp-01
cp bgp-01-r2.pcap ../assets/bgp-01/
Otherwise, delete it:
rm -f bgp-01-r2.pcap
What to Read Next
The next lab grows the topology to three ASes so you can watch the AS_PATH get longer, and introduces route withdrawal.
Next reading:
- RFC 4271 Section 4.3: UPDATE Message Format
- RFC 4271 Section 5.1.2: AS_PATH
- RFC 4271 Section 5.1.3: NEXT_HOP
- RFC 4271 Section 3.1: the description of withdrawn routes
References
- RFC 4271, Section 1.1: Definition of common BGP terms
- RFC 4271, Section 3: Summary of Operation
- RFC 4271, Section 3.1: Routes, UPDATE messages, and withdrawn routes
- RFC 4271, Section 4.2: OPEN Message Format
- RFC 4271, Section 4.3: UPDATE Message Format
- RFC 4271, Section 5.1.1: ORIGIN
- RFC 4271, Section 5.1.2: AS_PATH
- RFC 4271, Section 5.1.3: NEXT_HOP
- RFC 5737, Section 3: Documentation address blocks, including
203.0.113.0/24
Verified Run Log (2026-05-10)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 24.04 (
nkchan-desktop-1) - Docker 29.4.0
- containerlab 0.75.0 (no sudo needed if your user is in the
dockergroup) - FRRouting (containerlab's default
frrouting/frrimage)
Deploying examples/bgp-01/bgp-01.clab.yml as-is with containerlab deploy and waiting about 30 seconds gave the following state.
The BGP session as seen from r1
$ docker exec clab-bgp-01-r1 vtysh -c "show ip bgp summary"
IPv4 Unicast Summary (VRF default):
BGP router identifier 1.1.1.1, local AS number 65001 vrf-id 0
BGP table version 1
RIB entries 1, using 192 bytes of memory
Peers 1, using 717 KiB of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
10.0.0.2 4 65002 5 6 0 0 0 00:00:39 0 1 N/A
Total number of neighbors 1
PfxSnt 1 shows that r1 is advertising one prefix (203.0.113.0/24) to AS65002 (r2).
The BGP session as seen from r2
$ docker exec clab-bgp-01-r2 vtysh -c "show ip bgp summary"
IPv4 Unicast Summary (VRF default):
BGP router identifier 2.2.2.2, local AS number 65002 vrf-id 0
BGP table version 1
RIB entries 1, using 192 bytes of memory
Peers 1, using 717 KiB of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
10.0.0.1 4 65001 4 4 0 0 0 00:00:39 1 1 N/A
Total number of neighbors 1
PfxRcd 1 shows that r2 has received one prefix from AS65001 (r1).
The route r2 received
$ docker exec clab-bgp-01-r2 vtysh -c "show ip bgp"
BGP table version is 1, local router ID is 2.2.2.2, vrf id 0
Default local pref 100, local AS 65002
Status codes: s suppressed, d damped, h history, * valid, > best, = multipath,
i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found
Network Next Hop Metric LocPrf Weight Path
*> 203.0.113.0/24 10.0.0.1 0 0 65001 i
Displayed 1 routes and 1 total paths
The detailed view
$ docker exec clab-bgp-01-r2 vtysh -c "show ip bgp 203.0.113.0/24"
BGP routing table entry for 203.0.113.0/24, version 1
Paths: (1 available, best #1, table default)
Advertised to non peer-group peers:
10.0.0.1
65001
10.0.0.1 from 10.0.0.1 (1.1.1.1)
Origin IGP, metric 0, valid, external, best (First path received)
Last update: Sat May 9 20:49:23 2026
Mapping back to RFC 4271 terms
The goal this lab set at the start:
203.0.113.0/24 via 10.0.0.1, AS_PATH 65001, ORIGIN IGP
And how the real output lines up with it:
| RFC 4271 term | Real output |
|---|---|
| NLRI (prefix) | Network: 203.0.113.0/24 |
| NEXT_HOP | Next Hop: 10.0.0.1 |
| AS_PATH | Path: 65001 |
| ORIGIN | Origin IGP (or the i at the end of the route line) |
Cleanup
containerlab destroy -t bgp-01.clab.yml --cleanup
That's one full loop through BGP's core idea: a route is a prefix plus its path attributes, carried in an UPDATE, and you can now read every field of one against RFC 4271.
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 grow the topology to three ASes, watch the AS_PATH lengthen hop by hop, and see what a route withdraw looks like on the wire.