Skip to content

Build an OSPF lab with FRR in Docker

Build a three-router OSPF lab with FRRouting in Docker: two LANs, a shared backbone, DR and BDR election, and real neighbor and route output.

Published on 5 min read

You will build a small OSPF network on your laptop: three FRRouting (FRR) routers in Docker, a shared backbone, and two LANs with a host each. By the end you will have seen OSPF elect a DR and BDR, learn routes, and carry a ping from one LAN to the other. Every command and output below comes from a real run.

What you'll build

 host1 192.168.1.10                                  host2 192.168.2.10
   |  lan1 192.168.1.0/24                  lan2 192.168.2.0/24  |
   |                                                            |
 [r1] .254                                                .254 [r3]
   |  .11                                                  .13  |
   +-------------------- core 10.0.0.0/24 ---------------------+
                                |  .12
                              [r2]

 Loopbacks: r1 10.255.0.1   r2 10.255.0.2   r3 10.255.0.3   (all area 0)

The three routers share one backbone subnet, so OSPF treats it as a broadcast network and elects a Designated Router (DR). r1 and r3 each own a LAN. r2 only has its loopback, which is enough to show it in the routing tables.

What you need

  • Docker on Linux, or Docker Desktop on macOS or Windows. Tested on Docker 29.
  • The FRR image from the project's registry: quay.io/frrouting/frr:10.7.1.
  • The nicolaka/netshoot image for the hosts. It ships ping, traceroute and ip.

About 1 GB of disk for the images. No VMs, no nested virtualization.

Step 1: Create the networks

docker network create --subnet 192.168.1.0/24 ospf-lan1
docker network create --subnet 10.0.0.0/24    ospf-core
docker network create --subnet 192.168.2.0/24 ospf-lan2

Docker keeps the .1 address of each network for the host bridge. That is why the routers use .11 to .13 on the core and .254 on the LANs.

Step 2: Write the FRR configs

FRR reads two files from /etc/frr. The daemons file says which routing daemons start. The frr.conf file holds the configuration. Copy the image's default daemons file and switch ospfd on:

mkdir -p r1 r2 r3
for n in 1 2 3; do
  docker run --rm quay.io/frrouting/frr:10.7.1 cat /etc/frr/daemons \
    | sed 's/^ospfd=no/ospfd=yes/' > r$n/daemons
  touch r$n/vtysh.conf
done

Now write r1/frr.conf:

frr defaults traditional
hostname r1
!
interface lo
 ip address 10.255.0.1/32
!
router ospf
 ospf router-id 10.255.0.1
 network 10.255.0.1/32 area 0
 network 10.0.0.0/24 area 0
 network 192.168.1.0/24 area 0
 passive-interface lo
!

r2/frr.conf has no LAN:

frr defaults traditional
hostname r2
!
interface lo
 ip address 10.255.0.2/32
!
router ospf
 ospf router-id 10.255.0.2
 network 10.255.0.2/32 area 0
 network 10.0.0.0/24 area 0
 passive-interface lo
!

r3/frr.conf mirrors r1:

frr defaults traditional
hostname r3
!
interface lo
 ip address 10.255.0.3/32
!
router ospf
 ospf router-id 10.255.0.3
 network 10.255.0.3/32 area 0
 network 10.0.0.0/24 area 0
 network 192.168.2.0/24 area 0
 passive-interface lo
!

The network statements match interfaces by address, not by name. That matters in Docker: you can't always predict whether a network becomes eth0 or eth1, and with this style you don't have to.

Step 3: Start the routers

Create each router on the core network, attach the LANs, then start them:

for n in 1 2 3; do
  docker create --name ospf-r$n --hostname r$n --privileged \
    --sysctl net.ipv4.ip_forward=1 \
    --network ospf-core --ip 10.0.0.1$n \
    -v "$PWD/r$n:/etc/frr" quay.io/frrouting/frr:10.7.1
done
docker network connect --ip 192.168.1.254 ospf-lan1 ospf-r1
docker network connect --ip 192.168.2.254 ospf-lan2 ospf-r3
docker start ospf-r1 ospf-r2 ospf-r3

--sysctl net.ipv4.ip_forward=1 makes sure each router forwards packets between its interfaces. docker create plus docker network connect attaches every interface before FRR starts.

Step 4: Start the hosts

docker run -d --name ospf-host1 --hostname host1 --cap-add NET_ADMIN \
  --network ospf-lan1 --ip 192.168.1.10 nicolaka/netshoot:v0.13 sleep infinity
docker run -d --name ospf-host2 --hostname host2 --cap-add NET_ADMIN \
  --network ospf-lan2 --ip 192.168.2.10 nicolaka/netshoot:v0.13 sleep infinity

docker exec ospf-host1 ip route replace default via 192.168.1.254
docker exec ospf-host2 ip route replace default via 192.168.2.254

Docker points each container's default route at the bridge (.1). The last two commands send the hosts' traffic to their router instead. That is why the hosts need NET_ADMIN.

Step 5: Verify OSPF

Wait about 40 seconds. On a broadcast network, OSPF waits for the Wait timer (equal to the dead interval) before it elects the DR. Then check r1's neighbors:

docker exec ospf-r1 vtysh -c 'show ip ospf neighbor'
Neighbor ID     Pri State           Up Time         Dead Time Address         Interface                        RXmtL RqstL DBsmL
10.255.0.2        1 Full/Backup     5.666s            39.133s 10.0.0.12       eth0:10.0.0.11                       1     0     0
10.255.0.3        1 Full/DR         10.668s           39.334s 10.0.0.13       eth0:10.0.0.11                       0     0     0

Both neighbors are Full. r3 won the DR election and r2 is the BDR. When routers start together with equal priorities, the highest router ID wins, and r3 has the highest. From r2's side, r1 shows as Full/DROther.

Now the routes r1 learned through OSPF:

docker exec ospf-r1 vtysh -c 'show ip route ospf'
IPv4 unicast VRF default:
O>* 10.255.0.2/32 [110/10] via 10.0.0.12, eth0, weight 1, 00:00:06
O>* 10.255.0.3/32 [110/10] via 10.0.0.13, eth0, weight 1, 00:00:06
O>* 192.168.2.0/24 [110/20] via 10.0.0.13, eth0, weight 1, 00:00:06

[110/20] is the administrative distance (110 for OSPF) and the metric. Each interface in this lab has an OSPF cost of 10, so the far LAN costs 20: the core plus r3's LAN.

show ip ospf interface eth0 on r2 confirms the timers and roles:

  Router ID 10.255.0.2, Network Type BROADCAST, Cost: 10
  Transmit Delay is 1 sec, State Backup, Priority 1
  Designated Router (ID) 10.255.0.3 Interface Address 10.0.0.13/24
  Backup Designated Router (ID) 10.255.0.2, Interface Address 10.0.0.12
  Timer intervals configured, Hello 10s, Dead 40s, Wait 40s, Retransmit 5

Finally, end to end:

docker exec ospf-host1 ping -c 3 192.168.2.10
docker exec ospf-host1 traceroute -n 192.168.2.10
64 bytes from 192.168.2.10: icmp_seq=1 ttl=62 time=0.218 ms
64 bytes from 192.168.2.10: icmp_seq=2 ttl=62 time=0.118 ms
64 bytes from 192.168.2.10: icmp_seq=3 ttl=62 time=0.085 ms

traceroute to 192.168.2.10 (192.168.2.10), 30 hops max, 46 byte packets
 1  192.168.1.254  0.006 ms  0.004 ms  0.004 ms
 2  10.0.0.13  0.003 ms  0.004 ms  0.004 ms
 3  192.168.2.10  0.003 ms  0.006 ms  0.004 ms

A TTL of 62 means two routers on the way, and the traceroute shows them: r1, then r3.

What OSPF did

OSPF is a link-state protocol. Each router does three things:

  1. Finds neighbors. It sends hellos to 224.0.0.5 every 10 seconds on each OSPF interface. Routers that agree on area, subnet and timers become neighbors.
  2. Floods its links. Each router describes its own links in a Router LSA. On the broadcast core, the DR also sends a Network LSA that lists every router on that segment. Neighbors sync these into the same database.
  3. Runs SPF. Each router runs Dijkstra's shortest path algorithm over the database and installs the best routes.

You can see the database on any router:

docker exec ospf-r1 vtysh -c 'show ip ospf database'
                Router Link States (Area 0.0.0.0)

Link ID         ADV Router      Age  Seq#       CkSum  Link count
10.255.0.1     10.255.0.1       101 0x80000007 0x4e14 3
10.255.0.2     10.255.0.2       102 0x80000006 0x9d46 2
10.255.0.3     10.255.0.3       106 0x80000007 0x97c1 3

                Net Link States (Area 0.0.0.0)

Link ID         ADV Router      Age  Seq#       CkSum
10.0.0.13      10.255.0.3       106 0x80000002 0xbd4f

There is one Router LSA per router. r1 and r3 list 3 links (loopback, core, LAN) and r2 lists 2. The single Network LSA comes from r3, the DR, and its Link ID is r3's core address. All three routers hold this same database, so they all compute consistent routes.

Why a DR at all? Without one, every router on a shared segment would form a full adjacency with every other router. With a DR, each router syncs only with the DR and the BDR. On three routers the saving is small. On a LAN with twenty routers it matters.

If something doesn't work

  • No neighbors at all. Check that each router has its core address with docker exec ospf-r1 ip -br addr. A typo in a network line silently leaves an interface out of OSPF.
  • Neighbors stuck in ExStart or Exchange. This usually means an MTU mismatch. All Docker bridge networks default to 1500, so it only happens if you changed one.
  • Routes present but ping fails. Check the host's default route with ip route, and sysctl net.ipv4.ip_forward on the routers.
  • Address already in use. You asked for the .1 address, which belongs to the Docker bridge.

You may also notice a kernel default route on each router, K>* 0.0.0.0/0 via 10.0.0.1. Docker adds it. It doesn't affect the lab.

Clean up

docker rm -f ospf-r1 ospf-r2 ospf-r3 ospf-host1 ospf-host2
docker network rm ospf-lan1 ospf-core ospf-lan2

Going further

  • Change a cost. Add ip ospf cost 100 under an interface and watch the metric change in show ip route ospf.
  • Make LAN interfaces passive. No other router lives on the LANs, so passive-interface stops useless hellos there. Look up the interface name first with ip -br addr.
  • Add a second area. Put r3's LAN in area 1 and look at the summary LSAs in show ip ospf database.
  • Break things. Stop r3 with docker stop ospf-r3 and see how long r1 keeps the route. Then lower the hello and dead intervals.
  • Move on to BGP. The eBGP lab with FRR in Docker uses the same setup with three autonomous systems.

The FRR OSPFv2 documentation lists every command used here, and RFC 2328 is the protocol itself, including the DR election rules. The daemons file is described in the FRR setup guide.

If you build this lab with Docker nodes inside GNS3 instead and they can't reach the internet, see GNS3 Docker container has no internet.

Draw it instead

Typing docker network connect for each cable gets old fast. In Isothrum's editor you can draw the same network lab in the browser, with no account, and use FRR as the router. It exports an isoloom.yml plus Docker Compose files, and you run the lab with Docker as above. You still write the OSPF config yourself, which is the part worth learning.

Related articles

Build an eBGP lab with three FRR routers in Docker, one per AS. Bring up sessions, exchange prefixes, read the AS path and filter a route.
Set up an isolated security lab with DVWA as a target, an attacker box and a packet-capture sensor in Docker. Tested, with legal and isolation rules.
A three-zone DMZ lab in Docker: an nftables firewall between LAN, DMZ and outside, with DNAT to a web server, tested with real connection attempts.