DEV Community

pathvector-dev
pathvector-dev

Posted on • Originally published at blog.pathvector.dev

BGP From Scratch: Announce One Prefix and Explain Every Field of the Route

Originally published at https://blog.pathvector.dev/protocol-lab-bgp-01/ — 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 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
Enter fullscreen mode Exit fullscreen mode

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, and ORIGIN together 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What to notice in this diagram:

  • r1 and r2 are in different ASes, so this session is eBGP.
  • r1 originates 203.0.113.0/24.
  • r2 receives the UPDATE and installs a route to 203.0.113.0/24 in 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
  • tcpdump on 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you are only reading the article, create an empty working directory and build the files below:

mkdir -p bgp-01
cd bgp-01
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

3. Create the FRRouting configs

mkdir -p r1 r2
Enter fullscreen mode Exit fullscreen mode

r1/daemons:

zebra=yes
bgpd=yes
Enter fullscreen mode Exit fullscreen mode

r1/vtysh.conf:

service integrated-vtysh-config
Enter fullscreen mode Exit fullscreen mode

r2/daemons:

zebra=yes
bgpd=yes
Enter fullscreen mode Exit fullscreen mode

r2/vtysh.conf:

service integrated-vtysh-config
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Deploy

sudo containerlab deploy -t bgp-01.clab.yml
Enter fullscreen mode Exit fullscreen mode

Once it comes up, confirm the containers exist:

docker ps --format "table {{.Names}}\t{{.Status}}"
Enter fullscreen mode Exit fullscreen mode

What to check:

  • clab-bgp-01-r1 is running.
  • clab-bgp-01-r2 is 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"
Enter fullscreen mode Exit fullscreen mode

What to observe:

  • r1's AS is 65001.
  • r2's AS is 65002.
  • The peer reaches an Established-equivalent state.
  • On r2, PfxRcd (or State/PfxRcd) shows 1.
  • On r1, a received-prefix count of 0 is fine — r2 isn'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"
Enter fullscreen mode Exit fullscreen mode

How to read the expected output:

Network          Next Hop        Path
*> 203.0.113.0/24 10.0.0.1       65001 i
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

What to observe:

  • 65001 appears under Paths.
  • You see from 10.0.0.1 or nexthop 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
Enter fullscreen mode Exit fullscreen mode

What to check:

  • clab-bgp-01-r1 is listed.
  • clab-bgp-01-r2 is 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 OPEN
    • My AS: 65001
    • BGP Identifier: 1.1.1.1
  • BGP UPDATE
    • Path attributes
    • ORIGIN
    • AS_PATH: 65001
    • NEXT_HOP: 10.0.0.1
    • NLRI: 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
Enter fullscreen mode Exit fullscreen mode

What to look for:

  • The local AS is 65002.
  • The neighbor is 10.0.0.1.
  • The neighbor AS is 65001.
  • State/PfxRcd is 1, 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
Enter fullscreen mode Exit fullscreen mode

What to look for:

  • 203.0.113.0/24 is in the BGP table.
  • NEXT_HOP is 10.0.0.1.
  • AS_PATH is 65001.
  • ORIGIN is i (or IGP in 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 ...
Enter fullscreen mode Exit fullscreen mode

What to look for:

  • 65001 appears as the path.
  • You see from 10.0.0.1 or nexthop 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 Network column is not a next hop. It's the destination range you want to reach.
  • Next Hop is not the origin AS. It's the IP address packets are forwarded to next.
  • AS_PATH is not a list of physical router names. It's a sequence of AS numbers.
  • The i does not mean iBGP. It's the display for the ORIGIN attribute's IGP value.
  • 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

If you still don't see it, double-check which interface tcpdump is watching:

sudo ip netns exec clab-bgp-01-r2 ip link
Enter fullscreen mode Exit fullscreen mode

Check Your Understanding

  1. Looking at show bgp ipv4 unicast on r2, which parts of the 203.0.113.0/24 entry are the NLRI, AS_PATH, NEXT_HOP, and ORIGIN?
  2. If you changed r1's AS number to 65010, how would the AS_PATH seen from r2 change?
  3. If you removed network 203.0.113.0/24 from r1 and reloaded FRR, how would r2's BGP table change? Which RFC 4271 concept does that connect to?
  4. Explain in your own words why the NEXT_HOP is 10.0.0.1.
  5. What does ORIGIN = IGP actually 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
Enter fullscreen mode Exit fullscreen mode

Confirm no containers are left behind:

docker ps --format "table {{.Names}}\t{{.Status}}" | grep clab-bgp-01 || true
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

Otherwise, delete it:

rm -f bgp-01-r2.pcap
Enter fullscreen mode Exit fullscreen mode

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 docker group)
  • FRRouting (containerlab's default frrouting/frr image)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)