Originally published at https://blog.pathvector.dev/protocol-lab-bgp-02/ — 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 watch a single BGP route appear on r2, disappear after a withdrawal, and come back after it is announced again. The theme is deliberately simple: an UPDATE message can announce reachability, and an UPDATE message can withdraw reachability. Lab 01 showed you one route sitting in a BGP table; this lab revisits that same route as a pair of movements — advertisement and withdrawal — carried by UPDATE messages.
Reading guide: rfc-notes/bgp-rfc4271-lab02.md
Expected time: 45–60 minutes.
The Goal
By the end, you should be able to explain this sequence:
203.0.113.0/24 is visible on r2
203.0.113.0/24 is withdrawn by r1
203.0.113.0/24 disappears from r2
203.0.113.0/24 is announced again by r1
203.0.113.0/24 returns on r2
What You Will Learn
- A BGP UPDATE message is used for both route announcements and route withdrawals.
- In an announcement, the prefix goes into the NLRI, accompanied by path attributes such as ORIGIN, AS_PATH, and NEXT_HOP.
- In a withdrawal, the prefix being withdrawn goes into Withdrawn Routes.
- A withdrawal does not need to restate the route's AS_PATH or NEXT_HOP.
- You can confirm a route vanishing from the BGP table in both FRRouting output and a pcap.
This lab does not cover:
- competing origins
- route leaks
- RPKI / ROA / ROV
- the BGP decision process
- iBGP
- route reflectors
- public Internet announcements
Where to Read in the RFCs
The required reading for this lab is RFC 4271:
| Section | What to focus on |
|---|---|
| 3.1 | Routes are advertised — and withdrawn — via UPDATE |
| 4.3 | The UPDATE message's Withdrawn Routes, Path Attributes, and NLRI
|
| 5 | Mandatory attributes when an UPDATE carries NLRI |
| 5.1.1 | ORIGIN |
| 5.1.2 | AS_PATH |
| 5.1.3 | NEXT_HOP |
The Big Picture
We reuse the same two-router setup as Lab 01.
AS65001 / r1 AS65002 / r2
10.0.0.1/30 ---------------------- 10.0.0.2/30
Step 1:
r1 announces 203.0.113.0/24
r2 learns 203.0.113.0/24
Step 2:
r1 withdraws 203.0.113.0/24
r2 removes 203.0.113.0/24
Step 3:
r1 announces 203.0.113.0/24 again
r2 learns 203.0.113.0/24 again
203.0.113.0/24 is an RFC 5737 documentation prefix. It is never advertised externally — it only exists inside the lab.
sequenceDiagram
participant r1 as r1<br/>AS65001
participant r2 as r2<br/>AS65002
r1->>r2: UPDATE announce<br/>NLRI 203.0.113.0/24<br/>NEXT_HOP 10.0.0.1<br/>AS_PATH 65001
Note over r2: route is visible
r1->>r2: UPDATE withdraw<br/>Withdrawn Routes 203.0.113.0/24
Note over r2: route disappears
r1->>r2: UPDATE announce again<br/>NLRI 203.0.113.0/24
Note over r2: route returns
flowchart LR
announce["Announcement UPDATE<br/>Path Attributes<br/>ORIGIN / AS_PATH / NEXT_HOP<br/>NLRI 203.0.113.0/24"]
withdraw["Withdrawal UPDATE<br/>Withdrawn Routes<br/>203.0.113.0/24<br/>no NLRI needed for this withdrawal"]
table["r2 BGP table<br/>route appears<br/>route disappears<br/>route returns"]
announce --> table
withdraw --> 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 or tshark
Images used:
frrouting/frr:latest
On macOS, run this inside a Linux VM, a WSL-equivalent environment, or a Linux VM on OrbStack/Colima. Capturing BGP packets inside Linux network namespaces is much easier to work with for learning purposes.
Running the Lab
All of these steps run inside the Linux environment where containerlab is installed.
If you have the repo checked out, the quick path runs the verification script, which handles topology deploy, confirming the route appears, the withdraw, confirming it disappears, the re-announcement, the pcap capture, and the destroy:
./scripts/labctl.sh run bgp-02
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/bgp-02
2. Deploy
sudo containerlab deploy -t bgp-02.clab.yml
After it comes up, confirm the containers were created:
docker ps --format "table {{.Names}}\t{{.Status}}"
What to check:
-
clab-bgp-02-r1is running. -
clab-bgp-02-r2is running.
Give the neighbors a few seconds to reach Established, then look at the BGP summary:
docker exec -it clab-bgp-02-r2 vtysh -c "show bgp summary"
What to observe:
- Local AS is
65002. - The neighbor is
10.0.0.1. - The neighbor's AS is
65001. -
State/PfxRcdis1, or the output otherwise shows an Established session receiving one prefix.
3. Look at the route while it exists
docker exec -it clab-bgp-02-r2 vtysh -c "show bgp ipv4 unicast"
Expected reading:
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_HOP is
10.0.0.1. - AS_PATH is
65001. - ORIGIN is
i(shown asIGPin detailed output).
4. Start a packet capture
Open a second terminal — still inside the Linux environment running containerlab — and capture on r2's interface.
sudo ip netns list | grep clab-bgp-02
What to check:
-
clab-bgp-02-r1is listed. -
clab-bgp-02-r2is listed.
Start the capture:
sudo ip netns exec clab-bgp-02-r2 tcpdump -i eth1 -nn -s 0 -w bgp-02-r2.pcap tcp port 179
5. Withdraw the route
With the capture still running, remove the network statement from r1 in another terminal:
docker exec -it clab-bgp-02-r1 vtysh \
-c "configure terminal" \
-c "router bgp 65001" \
-c "address-family ipv4 unicast" \
-c "no network 203.0.113.0/24"
Wait a few seconds, then look at r2:
docker exec -it clab-bgp-02-r2 vtysh -c "show bgp ipv4 unicast"
What to check:
-
203.0.113.0/24is no longer displayed. - The neighbor may stay Established — what disappeared was the route, not the session.
6. Re-announce the route
On the same r1, put the network statement back:
docker exec -it clab-bgp-02-r1 vtysh \
-c "configure terminal" \
-c "router bgp 65001" \
-c "address-family ipv4 unicast" \
-c "network 203.0.113.0/24"
Wait a few seconds, then check r2 again:
docker exec -it clab-bgp-02-r2 vtysh -c "show bgp ipv4 unicast"
What to check:
-
203.0.113.0/24is back. - NEXT_HOP is
10.0.0.1. - AS_PATH is
65001. - ORIGIN is
i/IGP.
Stop tcpdump with Ctrl-C. The file bgp-02-r2.pcap remains in your current working directory.
7. Inspect the UPDATEs in the pcap
Open bgp-02-r2.pcap in Wireshark.
Because the capture was taken on r2's interface, you may see UPDATEs in both directions (r1 -> r2 and r2 -> r1). Focus first on the UPDATEs from 10.0.0.1 -> 10.0.0.2.
Where to look:
- announce UPDATE
Path attributesORIGINAS_PATH: 65001NEXT_HOP: 10.0.0.1NLRI: 203.0.113.0/24
- withdraw UPDATE
Withdrawn Routes203.0.113.0/24
Key points:
- For the announcement, look at
Path Attributes + NLRI. - For the withdrawal, look at
Withdrawn Routes. - The withdrawal is not a message that restates NEXT_HOP or AS_PATH.
If you want to open the pcap in Wireshark on macOS, move the generated bgp-02-r2.pcap to the Mac via a shared directory or scp first.
Expected Output
The point here is confirming the route's state changes, not matching output byte-for-byte.
Before the withdraw
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_HOP is
10.0.0.1. - AS_PATH is
65001.
After the withdraw
On r2:
Displayed 0 routes and 0 total paths
The exact wording varies a little between FRRouting versions. What matters is that 203.0.113.0/24 is no longer displayed.
After the re-announcement
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 back. - NEXT_HOP and AS_PATH match the original announcement.
Why It Works
The UPDATE message defined in RFC 4271 Section 4.3 has three key fields: Withdrawn Routes, Path Attributes, and Network Layer Reachability Information (NLRI).
While r1 has network 203.0.113.0/24 configured, it advertises 203.0.113.0/24 as NLRI. That UPDATE carries path attributes such as ORIGIN, AS_PATH, and NEXT_HOP.
When you run no network 203.0.113.0/24 on r1, it withdraws the reachability it previously advertised. In the withdrawal UPDATE, the prefix being withdrawn goes into Withdrawn Routes. r2 receives that UPDATE and removes the matching route from its BGP table.
Configure network 203.0.113.0/24 again, and r1 re-advertises the same prefix. r2 receives the new announcement and puts the route back in its BGP table.
Common Misconceptions
- A withdraw does not tear down the BGP session. The neighbor can stay Established while only the route disappears.
- A withdraw is not "sending an unreachable next hop." It lists the withdrawn prefix in Withdrawn Routes.
-
no network 203.0.113.0/24does not delete the loopback's IP address. It removes the configuration that originates the prefix into BGP. - When
203.0.113.0/24vanishes from the BGP table, nothing was advertised to — or withdrawn from — the real internet. This lab runs entirely inside a closed Docker environment.
Common Pitfalls
The route is still there after the withdraw
Wait a few seconds and check again:
docker exec -it clab-bgp-02-r2 vtysh -c "show bgp ipv4 unicast"
If it's still there, check r1's running config:
docker exec -it clab-bgp-02-r1 vtysh -c "show running-config"
If network 203.0.113.0/24 is still present under address-family ipv4 unicast, the withdraw never actually happened.
The route doesn't come back after re-announcing
Check whether r1 actually holds 203.0.113.0/24 locally:
docker exec -it clab-bgp-02-r1 ip addr show lo
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.
The withdraw doesn't show up in the pcap
If you withdraw before starting the capture, you miss the withdrawal UPDATE. Start tcpdump first, and only then run no network 203.0.113.0/24.
Also double-check which interface tcpdump is capturing on:
sudo ip netns exec clab-bgp-02-r2 ip link
Check Your Understanding
- In the announcement UPDATE, which part carries
203.0.113.0/24? - In the withdrawal UPDATE, which part carries
203.0.113.0/24? - Explain, in your own words, why AS_PATH and NEXT_HOP are not required in a withdrawal.
- Explain why the BGP neighbor can stay Established after the withdraw.
- Compare
no network 203.0.113.0/24withip addr del 203.0.113.1/24 dev lo: what looks similar in the observed results, and what differs?
Cleanup
When you're done, destroy the containerlab topology:
sudo containerlab destroy -t bgp-02.clab.yml
Confirm no containers are left behind:
docker ps --format "table {{.Names}}\t{{.Status}}" | grep clab-bgp-02 || true
If you want to keep the pcap, move it into assets/bgp-02/:
mkdir -p ../../assets/bgp-02
cp bgp-02-r2.pcap ../../assets/bgp-02/
Otherwise, delete it:
rm -f bgp-02-r2.pcap
What to Read Next
The next lab constructs a state where the same prefix is visible from different origin ASes — the entry point to competing origins and route leaks.
Read next:
- RFC 4271 Section 5.1.2, AS_PATH
- RFC 4271 Section 9, UPDATE Message Handling
- As a doorway into RPKI / ROA: the question of which AS is allowed to originate a prefix
References
- RFC 4271, Section 3.1: Routes, advertisement, and withdrawal
- RFC 4271, Section 4.3: UPDATE Message Format
- RFC 4271, Section 5: Path Attributes
- 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-08, from assets/bgp-02/runs/20260508T232252Z/run.log)
This lab has been confirmed reproducible on real hardware.
Verification results:
- run id:
20260508T232252Z - status:
verified - Observed:
203.0.113.0/24appeared onr2, disappeared on withdraw, and came back on re-advertise. - capture:
assets/bgp-02/runs/20260508T232252Z/bgp-02-r2.pcap
The run.log doesn't include raw show bgp summary output, so what follows quotes the BGP table observations and the verification script's state-transition log from that same log. The whole point of this lab is confirming that the route alone was withdrawn and re-advertised over an Established session — the BGP session itself never went down.
Verification summary
[protocol-lab][bgp-02] waiting for BGP route on r2: before withdraw
[protocol-lab][bgp-02] BGP route present after 3s: before withdraw
...
[protocol-lab][bgp-02] withdrawing 203.0.113.0/24 from r1
[protocol-lab][bgp-02] waiting for BGP route withdrawal on r2: after withdraw
[protocol-lab][bgp-02] BGP route absent after 2s: after withdraw
...
[protocol-lab][bgp-02] announcing 203.0.113.0/24 from r1
[protocol-lab][bgp-02] waiting for BGP route on r2: after reannounce
[protocol-lab][bgp-02] BGP route present after 1s: after reannounce
...
[protocol-lab][bgp-02] verification OK
This flow maps directly onto RFC 4271 Section 3.1's statement that routes are advertised — and withdrawn — via UPDATE.
Before the withdraw: r2 holds the route
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
How to read it:
-
Network 203.0.113.0/24is the prefix advertised as the UPDATE's NLRI. -
Next Hop 10.0.0.1is the NEXT_HOP from RFC 4271 Section 5.1.3. -
Path 65001is the AS_PATH from RFC 4271 Section 5.1.2. - The trailing
iis ORIGIN = IGP from RFC 4271 Section 5.1.1.
After the withdraw: the route vanishes from r2
[protocol-lab][bgp-02] withdrawing 203.0.113.0/24 from r1
[protocol-lab][bgp-02] waiting for BGP route withdrawal on r2: after withdraw
[protocol-lab][bgp-02] BGP route absent after 2s: after withdraw
No BGP prefixes displayed, 0 exist
In the withdrawal UPDATE, the prefix being withdrawn goes into the Withdrawn Routes field of RFC 4271 Section 4.3. Here 203.0.113.0/24 was the withdrawal target, and the matching route disappeared from r2's BGP table.
Crucially, a withdraw is not a message that re-advertises AS_PATH or NEXT_HOP as some "unreachable" value. Listing the prefix in Withdrawn Routes is what invalidates the previously advertised reachability.
After the re-advertise: the route returns on r2
[protocol-lab][bgp-02] announcing 203.0.113.0/24 from r1
[protocol-lab][bgp-02] waiting for BGP route on r2: after reannounce
[protocol-lab][bgp-02] BGP route present after 1s: after reannounce
BGP table version is 3, 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
On re-advertise, 203.0.113.0/24 is announced as NLRI once more, arriving at r2 together with its path attributes — ORIGIN, AS_PATH, NEXT_HOP. The same route lands back in r2's BGP table.
Mapping observations to RFC 4271 terminology
| Observation | RFC 4271 term | What this lab showed |
|---|---|---|
| route announcement | UPDATE message with NLRI and Path Attributes |
203.0.113.0/24, 10.0.0.1, 65001 i
|
| NLRI | Network Layer Reachability Information | Network 203.0.113.0/24 |
| NEXT_HOP | NEXT_HOP path attribute | Next Hop 10.0.0.1 |
| AS_PATH | AS_PATH path attribute | Path 65001 |
| ORIGIN | ORIGIN path attribute |
i / IGP |
| route withdrawal | UPDATE message with Withdrawn Routes |
BGP route absent, No BGP prefixes displayed, 0 exist
|
| re-advertise | new UPDATE announcement after withdrawal | table version changes to 3, route appears again |
Cleanup
The verification script destroys the topology at the end:
[protocol-lab][bgp-02] destroying topology
08:23:08 INFO Destroying lab name=bgp-02
08:23:08 INFO Removed container name=clab-bgp-02-r1
08:23:08 INFO Removed container name=clab-bgp-02-r2
That's the heart of BGP dynamics: one message type, UPDATE, carries both the "here's a route" and the "forget that route" — and you've now seen both on the wire.
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 create a state where the same prefix is announced from two different origin ASes — the doorway to competing origins, route leaks, and eventually RPKI.
Top comments (0)