A router does not plan the whole path to a destination. It picks the most specific route in its own table, hands the packet to the next hop, and every router along the way repeats that decision on its own. That independence is why a link that failed silently cost us 39.96 seconds of lost traffic under OSPF’s default timers, and only 0.73 seconds once BFD watched the same link.
This guide explains IP routing and routing protocols: how a router chooses between routes, when static routes are enough, and how OSPF, BGP, RIP and EIGRP compare. You then build it on one Linux box, four routers and two hosts as network namespaces, with static routes, OSPF and eBGP in FRRouting, plus the errors we hit. We ran every command below with FRRouting 10.3 on a Debian 13 VM in October 2026, so every output is real.
What IP routing and routing protocols do
IP routing moves a packet from the network it starts on to the network it’s addressed to, one router at a time. Each router reads the destination address in the IP header, finds the best match in its routing table, and sends the packet out of the matching interface toward the next hop. No router holds the full path, and none needs to, because each one only has to move the packet one step closer.
Routing is a Layer 3 job, so routers decide on IP prefixes and never look at MAC addresses for anything but the final delivery on each link. If the layer numbers are still fuzzy, the OSI and TCP/IP model explainer covers where IP sits, and the guide to routers, switches and firewalls explains why a switch cannot do this job and a router can.
When you read a routing table, every entry answers the same four questions, and a missing answer is usually where a routing problem starts. You will see all four in every table below, so it pays to read them in this order. Each entry holds:
- the destination prefix, such as 10.0.20.0/24
- the next-hop address, or “directly connected”
- the outgoing interface
- where the route came from: connected, static, or a protocol like OSPF
That last field matters more than it looks, because it decides which route wins when two sources disagree. This is the kernel table on r1 from the lab you build later in this guide, taken after OSPF had converged.
10.0.10.0/24 dev to-h1 proto kernel scope link src 10.0.10.1
10.0.12.0/30 dev to-r2 proto kernel scope link src 10.0.12.1
10.0.13.0/30 dev to-r3 proto kernel scope link src 10.0.13.1
10.0.20.0/24 nhid 19 via 10.0.12.2 dev to-r2 proto ospf metric 20
10.0.23.0/30 nhid 19 via 10.0.12.2 dev to-r2 proto ospf metric 20
The three proto kernel lines are connected networks that appeared the moment the interfaces got addresses. The two proto ospf lines were learned from neighbors and pushed into the kernel by FRRouting. Do not read metric 20 as the OSPF cost: FRR installs every OSPF route with that kernel metric, and the real cost (30 for 10.0.20.0/24) lives in FRR’s own table.
How a router chooses the best route
A router applies three rules, always in the same order: the longest matching prefix wins, then the lowest administrative distance among sources offering that exact prefix, then the lowest metric within one protocol. Each rule only breaks ties left by the one before it, so a /24 learned by RIP beats a /16 learned by OSPF, no matter how good the OSPF metric is.
Longest prefix match is the rule you are most likely to forget. In our lab, r1 held a broad 10.0.0.0/8 route via r3 and a specific 10.0.20.0/24 route via r2. A packet for 10.0.20.10 matched both and went to r2, because /24 is longer than /8. A packet for 10.0.99.5 only matched the /8, so it went to r3. Default routes (0.0.0.0/0) are simply the shortest prefix possible, which is why your default route only catches traffic nothing else claims.
Lower is more trusted, so a static route beats OSPF for the same prefix unless you raise its distance on purpose. Administrative distance (AD) ranks route sources by how much the router trusts them. These are Cisco’s documented defaults from the administrative distance reference, next to what our FRR lab showed:
| Route source | Default AD (Cisco IOS) | Seen in the FRR 10.3 lab |
|---|---|---|
| Connected interface | 0 | Connected routes always selected |
| Static route | 1 | Not tested at default (we set 200) |
| External BGP | 20 | 20, on r4’s BGP routes |
| Internal EIGRP | 90 | Not tested |
| OSPF | 110 | 110 |
| IS-IS | 115 | Not tested |
| RIP | 120 | Not tested |
| External EIGRP | 170 | Not tested |
| Internal BGP | 200 | Not tested |
The practical use of AD is the floating static route: a static route with a distance higher than your routing protocol’s, so it stays out of the table until the protocol loses the route. The lab section shows FRR keeping exactly that kind of backup route parked behind OSPF.
You only need the metric when one protocol offers several paths to the same prefix. OSPF adds up interface costs, so r1 had two candidates for 10.0.20.0/24: cost 30 through r2 and cost 110 over the direct link to r3. It installed the cost 30 path and kept the other as an alternative that took over when the r2 link failed.
Use static routes or dynamic routing
Static routes are the right answer when your network has only one way out. A branch office with a single uplink, a server that needs one extra route to a management network, and a default route toward an ISP all have no alternative path to fail over to. A routing protocol there adds moving parts and no benefit, while static routes cost nothing on the wire and never flap.
They stop scaling once you have more than one path. To move traffic between just two hosts across three routers, our static lab needed four routes, one in each direction on every hop. Traceroute still showed a silent hop until we added a fifth route back to a transit link, because a router’s own replies come from addresses nobody had routed. A link failure then needs you to rewrite the routes by hand. OSPF advertised every transit link on its own, found the backup path, and moved traffic within seconds, which is the whole argument for a dynamic protocol.
Most networks you will meet run both: a static default route toward the internet edge and a dynamic protocol for everything inside. On Cisco gear the same ideas use different syntax, which the IPv4 static routes walkthrough covers, including floating statics. Floating statics fill the gap as last-resort backups. You get predictable behavior at the edge, where there is one exit anyway, and automatic failover inside, where there are several paths.
Tell distance-vector, link-state and path-vector apart
Dynamic routing protocols fall into three families. Knowing the family tells you what to expect during an outage, so you can predict convergence before you measure it. The family tells you how a protocol behaves when something breaks, which matters more day to day than its feature list:
- Distance-vector: RIP, and EIGRP as an advanced variant
- Link-state: OSPF and IS-IS
- Path-vector: BGP
A distance-vector router only knows what its neighbors tell it: a prefix and a distance. It never sees the topology, so bad news spreads slowly and needs timers and hop limits to avoid routing loops. RIP caps the path at 15 hops for exactly that reason. EIGRP keeps the neighbor-told model but adds the DUAL algorithm, which precomputes a loop-free backup (a feasible successor) so it can switch without asking around.
A link-state router floods descriptions of its own links to every router in the area, so each router holds the same map and runs Dijkstra’s shortest-path algorithm on it. That costs more memory and CPU, but a change is flooded and recalculated in well under a second, which our carrier-loss test confirms: zero pings lost at 50 ms intervals when the r1 to r2 link went down.
Path-vector is BGP’s model. Each route carries the list of autonomous systems (AS) it crossed, which prevents loops between networks run by different organizations. The best path is chosen by policy (local preference, AS path length, MED and more) rather than by a single cost, so BGP runs the internet and deliberately converges slower than an interior protocol.
Choose between OSPF, BGP, RIP and EIGRP
OSPF is the default interior protocol for a multi-vendor network, BGP is the protocol between networks, RIP only survives in small or legacy setups, and EIGRP belongs to Cisco shops. Pick by where the routes need to go: inside one organization you want OSPF, and across an organizational boundary you need BGP. This table sets them side by side, with what we actually ran:
| Protocol | Family | Metric | Default AD (Cisco) | Runs over | Where it fits | In this lab |
|---|---|---|---|---|---|---|
| OSPF | Link-state | Cost per interface | 110 | IP protocol 89 | Campus, data center, multi-site enterprise | Tested on FRR 10.3 |
| BGP | Path-vector | Policy and path attributes | 20 eBGP / 200 iBGP | TCP 179 | Between autonomous systems, ISPs, DC fabrics | Tested (eBGP) |
| RIP | Distance-vector | Hop count, 15 max | 120 | UDP 520 | Tiny flat networks, legacy gear | Not tested |
| EIGRP | Advanced distance-vector | Bandwidth and delay | 90 internal / 170 external | IP protocol 88 | Cisco-only enterprise networks | Not tested |
OSPF splits a network into areas around a backbone, area 0, so a change in one area does not trigger recalculation everywhere. Neighbors find each other with hellos every 10 seconds and declare a neighbor dead after 40 seconds of silence, which are FRR’s documented defaults and the values our lab’s interfaces showed. That 40 second dead interval is exactly the outage we measured when a link stayed up but stopped passing traffic. For the Cisco side of the same protocol, the OSPF concepts guide walks through router IDs, DR election and LSA types.
BGP sessions run over TCP and exchange only what your policy allows. FRR enforces RFC 8212 on eBGP by default in its traditional profile: until a session has an inbound and outbound policy, it accepts and sends nothing, and show bgp summary prints (Policy) instead of a prefix count. We hit that on the first try, and the BGP section below shows both the symptom and the fix.
RIP is the easiest to understand and the least you’d want in production, since 30-second periodic updates and the 15-hop ceiling make it slow and small. EIGRP is Cisco’s protocol, published as informational RFC 7868. FRR ships an eigrpd daemon that its documentation describes against that RFC, but we didn’t test it, and Cisco IOS remains the reference implementation for anything EIGRP. If you’re working through the Cisco CCNA 200-301 material, where OSPF and static routing carry most of the routing weight, the CCNA preparation books we recommend cover the IOS command side that this Linux lab does not.
Build a routing lab with Linux network namespaces
A network namespace is a separate copy of the Linux network stack, with its own interfaces, routing table and sysctl settings. Six namespaces joined by veth pairs give you four routers and two hosts on one VM, with no extra machines and no changes to the hypervisor’s networking. The lab needs only a few things:
- a Debian 13 server or VM with sudo access (ours had 4 vCPUs and 6 GB because GNS3 shared it; the namespace lab itself runs comfortably in 1 GB)
- the iproute2 tools, which Debian installs by default
- the
frr,tracerouteandiputils-pingpackages
Everything below runs inside namespaces, so it cannot touch your VM’s real interface or routes. The topology is a triangle of routers r1, r2 and r3 running OSPF, with r4 in a second autonomous system peering with r3 over eBGP:

From r1 to r3, the path through r2 costs 20 and the direct link costs 100, so OSPF keeps the direct link as a backup. All addresses come from 10.0.0.0/8, 192.0.2.0/24 and 198.51.100.0/24, the last two being documentation ranges that never appear on the internet.
Install the packages first. FRR provides the routing daemons, and minimal Debian images may ship without ping or traceroute, which every check below uses. Nothing here starts a service you need to configure yet, because FRR stays idle until its daemons are enabled.
sudo apt update && sudo apt install -y frr traceroute iputils-ping
Debian 13 ships FRRouting 10.3 (package 10.3-3+deb13u1), which is what every capture here comes from. FRR’s own repository carries newer releases, but the Debian package gets security updates with the rest of the system, so we would stay on it for a lab. You will confirm the running version once the routers are up. The build script creates the namespaces, joins them with veth pairs, and assigns addresses. Keeping the topology in a script means you can tear it down and rebuild it in seconds, which you will do once before the OSPF section. Open a new file in your home directory:
vim build-lab.sh
Paste the script exactly as we ran it. It stops at the first error because of set -e, so a typo shows up immediately instead of leaving half a topology behind.
#!/bin/bash
# Four routers and two hosts as network namespaces joined by veth pairs
set -e
for ns in h1 r1 r2 r3 r4 h2; do
ip netns add "$ns"
ip -n "$ns" link set lo up
done
# link <ns-a> <if-a> <ns-b> <if-b>
link() {
ip link add "$2" netns "$1" type veth peer name "$4" netns "$3"
ip -n "$1" link set "$2" up
ip -n "$3" link set "$4" up
}
link h1 to-r1 r1 to-h1
link r1 to-r2 r2 to-r1
link r2 to-r3 r3 to-r2
link r1 to-r3 r3 to-r1
link r3 to-h2 h2 to-r3
link r3 to-r4 r4 to-r3
ip -n h1 addr add 10.0.10.10/24 dev to-r1
ip -n r1 addr add 10.0.10.1/24 dev to-h1
ip -n r1 addr add 10.0.12.1/30 dev to-r2
ip -n r2 addr add 10.0.12.2/30 dev to-r1
ip -n r2 addr add 10.0.23.1/30 dev to-r3
ip -n r3 addr add 10.0.23.2/30 dev to-r2
ip -n r1 addr add 10.0.13.1/30 dev to-r3
ip -n r3 addr add 10.0.13.2/30 dev to-r1
ip -n r3 addr add 10.0.20.1/24 dev to-h2
ip -n h2 addr add 10.0.20.10/24 dev to-r3
ip -n r3 addr add 192.0.2.1/30 dev to-r4
ip -n r4 addr add 192.0.2.2/30 dev to-r3
# r4 stands in for another network (AS 65002) that owns 198.51.100.0/24
ip -n r4 link add lan0 type dummy
ip -n r4 addr add 198.51.100.1/24 dev lan0
ip -n r4 link set lan0 up
ip -n h1 route add default via 10.0.10.1
ip -n h2 route add default via 10.0.20.1
The interface names say where each link leads, so you can read to-r2 on r1 as the cable to r2. Do not rename a host interface to eth0: ip link add checks the new name against the namespace you run it from, where the VM’s own eth0 already exists, so it fails (the troubleshooting section shows that error). A matching teardown script removes everything in one go, which you’ll use before the OSPF section:
vim teardown-lab.sh
It only deletes the six namespaces. Their veth pairs go with them, because a veth pair cannot outlive the namespace that holds one of its ends.
#!/bin/bash
for ns in h1 r1 r2 r3 r4 h2; do ip netns del "$ns" 2>/dev/null; done
Run the build and confirm the namespaces exist. It needs sudo because creating namespaces and interfaces is a root operation, so a permission error here means the sudo prefix was dropped.
sudo bash build-lab.sh && ip netns list
All six should be listed, newest first. A missing name means the script stopped early, and the error it printed tells you which link failed. If you see leftovers from an earlier attempt, run the teardown script first, because the build refuses to create a namespace that already exists.
h2
r4
r3
r2
r1
h1
Check r1’s interfaces. This is your first proof that the veth pairs landed in the right namespaces, so check it before adding any routes. Each one should be UP with its address:
sudo ip -n r1 -br addr
The @if2 suffix names the peer interface index in the other namespace. Every interface should read UP; a DOWN entry means the link set up line for that pair did not run.
lo UNKNOWN 127.0.0.1/8 ::1/128
to-h1@if2 UP 10.0.10.1/24 fe80::54ee:a6ff:fe72:2cc8/64
to-r2@if2 UP 10.0.12.1/30 fe80::d033:b3ff:fea4:959/64
to-r3@if3 UP 10.0.13.1/30 fe80::6820:83ff:fe88:cfa4/64
The addresses match the topology diagram, so the wiring is ready for routing.
Turn on IP forwarding in the routers
A new namespace copies its IPv4 settings from the host, and on our VM that meant net.ipv4.ip_forward = 0, so r1 to r4 behaved like hosts and dropped anything not addressed to them. On a host that already forwards (Docker turns it on, for example) the namespaces start at 1, and you will not see the failure described in the troubleshooting section. Turn forwarding on in each router namespace (not in h1 and h2, which are hosts):
for r in r1 r2 r3 r4; do sudo ip netns exec $r sysctl -w net.ipv4.ip_forward=1; done
Each router confirms the new value. You only need this on the routers, because h1 and h2 never forward traffic for anyone else.
net.ipv4.ip_forward = 1
net.ipv4.ip_forward = 1
net.ipv4.ip_forward = 1
net.ipv4.ip_forward = 1
On a real Linux router, persist the same setting in a file under /etc/sysctl.d/. In the namespaces it only lasts until you delete them, which is fine for a lab. Skip the file here, because the namespaces themselves disappear when you tear the lab down.
Route between the hosts with static routes
You need a route on every router to the networks it is not connected to, in both directions, so h1’s packets reach h2 and h2’s replies find their way back. Add the forward path through r2:
sudo ip -n r1 route add 10.0.20.0/24 via 10.0.12.2
sudo ip -n r2 route add 10.0.20.0/24 via 10.0.23.2
sudo ip -n r2 route add 10.0.10.0/24 via 10.0.12.1
sudo ip -n r3 route add 10.0.10.0/24 via 10.0.23.1
r1 also needs a route to the r2 to r3 transit link. Pings work without it, but r3’s traceroute replies come from 10.0.23.2, and r1 drops them because it has no route back to that address:
sudo ip -n r1 route add 10.0.23.0/30 via 10.0.12.2
Ping h2 from h1 to prove the round trip works. A reply needs both directions to be routed, so a success here tests all four routes at once.
sudo ip netns exec h1 ping -c 3 10.0.20.10
All three replies come back with a TTL of 61, because the reply crossed three routers that each decremented it from 64.
PING 10.0.20.10 (10.0.20.10) 56(84) bytes of data.
64 bytes from 10.0.20.10: icmp_seq=1 ttl=61 time=0.059 ms
64 bytes from 10.0.20.10: icmp_seq=2 ttl=61 time=0.055 ms
64 bytes from 10.0.20.10: icmp_seq=3 ttl=61 time=0.075 ms
--- 10.0.20.10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2036ms
rtt min/avg/max/mdev = 0.055/0.063/0.075/0.008 ms
Traceroute shows the path hop by hop, which is how you confirm the traffic took the route you intended and not just some route. It works by sending probes with a TTL of 1, 2, 3 and so on, so each router in turn reports itself.
sudo ip netns exec h1 traceroute -n 10.0.20.10
Each line is one router answering with an ICMP time exceeded message as the TTL runs out, ending at h2. Three hops plus the destination is what you should expect, because the forward path crosses r1, r2 and r3.
traceroute to 10.0.20.10 (10.0.20.10), 30 hops max, 60 byte packets
1 10.0.10.1 0.038 ms 0.004 ms 0.004 ms
2 10.0.12.2 0.013 ms 0.005 ms 0.005 ms
3 10.0.23.2 0.015 ms 0.007 ms 0.006 ms
4 10.0.20.10 0.015 ms 0.007 ms 0.007 ms
That path through r2 is exactly what the static routes describe, so the lab matches the plan.
Watch longest prefix match pick the route
Add a broad 10.0.0.0/8 route on r1 that points the other way, through r3. It overlaps the existing /24, which is exactly the situation longest prefix match exists to resolve. You would never leave this route in place on a real router, so treat it as a demonstration only.
sudo ip -n r1 route add 10.0.0.0/8 via 10.0.13.2
Now ask the kernel which route it would use for two destinations. That makes it the quickest way for you to check a routing decision without generating traffic. ip route get runs the real lookup without sending a packet:
sudo ip -n r1 route get 10.0.20.10
sudo ip -n r1 route get 10.0.99.5
The first destination matches both the /8 and the /24, and the /24 wins, so it still goes to r2. The second only matches the /8 and goes to r3:
10.0.20.10 via 10.0.12.2 dev to-r2 src 10.0.12.1 uid 0
cache
10.0.99.5 via 10.0.13.2 dev to-r3 src 10.0.13.1 uid 0
cache
A destination outside every route fails outright when you look it up, since there is no default route on r1. sudo ip -n r1 route get 192.0.2.2 returned RTNETLINK answers: Network is unreachable. That is the behavior you want from a router with no default: drop and report, rather than guess.
Run OSPF on the routers with FRRouting
FRRouting (FRR) is the routing suite most Linux routers use: one daemon per protocol (ospfd, bgpd, bfdd and others) plus zebra, which writes the chosen routes into the kernel. Its [email protected] unit can run a separate instance per namespace, and with watchfrr_options="--netns" in an instance’s daemons file, every daemon of that instance runs inside the namespace of the same name. The FRR setup documentation describes this pathspace mode.
Start from a clean topology so the only routes are connected ones, because leftover static routes would win over OSPF on distance and hide what the protocol is doing. Then turn forwarding back on.
sudo bash teardown-lab.sh && sudo bash build-lab.sh
for r in r1 r2 r3 r4; do sudo ip netns exec $r sysctl -w net.ipv4.ip_forward=1; done
Each router gets its own frr.conf. Point-to-point network type on the router links skips the DR election OSPF would otherwise run on an Ethernet segment, and passive on the host-facing interfaces advertises those networks without sending hellos to the hosts. Create a directory for the configs and open r1’s:
mkdir -p frr && vim frr/r1.conf
r1 advertises its host LAN and runs OSPF on both router links. The cost of 100 on the direct link to r3 makes that link the backup, since the path through r2 adds up to only 20.
frr defaults traditional
hostname r1
log syslog informational
!
interface to-h1
ip ospf area 0
ip ospf passive
!
interface to-r2
ip ospf area 0
ip ospf network point-to-point
!
interface to-r3
ip ospf area 0
ip ospf network point-to-point
ip ospf cost 100
!
router ospf
ospf router-id 10.255.0.1
!
r2 only has the two router links, so its file is the shortest. It has no host network of its own, so passive interfaces are not needed.
vim frr/r2.conf
There is nothing to advertise beyond the two transit networks. Both interfaces form adjacencies, because r2 sits in the middle of the preferred path.
frr defaults traditional
hostname r2
log syslog informational
!
interface to-r1
ip ospf area 0
ip ospf network point-to-point
!
interface to-r3
ip ospf area 0
ip ospf network point-to-point
!
router ospf
ospf router-id 10.255.0.2
!
r3 mirrors r1, with h2’s network passive and the same cost 100 on its side of the direct link. OSPF uses the cost of the outgoing interface, so both ends need it:
vim frr/r3.conf
Add the OSPF config. Keeping BGP out of the file at first means you can bring OSPF up and check it before the second protocol complicates the picture. BGP is added later from the FRR shell:
frr defaults traditional
hostname r3
log syslog informational
!
interface to-h2
ip ospf area 0
ip ospf passive
!
interface to-r1
ip ospf area 0
ip ospf network point-to-point
ip ospf cost 100
!
interface to-r2
ip ospf area 0
ip ospf network point-to-point
!
router ospf
ospf router-id 10.255.0.3
!
r4 doesn’t run OSPF at all. Running OSPF across the AS boundary would leak your internal topology to another network, so BGP is the only protocol on that link. It’s the router of the other autonomous system and only speaks BGP:
vim frr/r4.conf
It originates 198.51.100.0/24 and points a neighbor at r3. The neighbor statement alone is enough to start the session, so r4 is ready as soon as r3 is configured.
frr defaults traditional
hostname r4
log syslog informational
!
router bgp 65002
bgp router-id 10.255.0.4
neighbor 192.0.2.1 remote-as 65001
!
address-family ipv4 unicast
network 198.51.100.0/24
exit-address-family
!
A setup script creates one FRR instance per router, enables the daemons each one needs, and copies the configs in place. It stops the default FRR instance, so run it on a lab machine, not on a host that already routes with FRR:
vim frr-setup.sh
Run it from the directory that holds frr/, because it copies the configs from there. We added the [ -d frr ] guard line after our run, so a script started from the wrong directory exits before it stops the stock FRR service. Run it with sudo because it writes under /etc/frr, which only root and the frr user can change.
#!/bin/bash
# One FRR instance per router namespace; run from the directory that holds frr/
set -e
[ -d frr ] || { echo "run from the dir holding frr/"; exit 1; }
systemctl disable --now frr
for r in r1 r2 r3 r4; do
mkdir -p /etc/frr/$r
cp /etc/frr/daemons /etc/frr/vtysh.conf /etc/frr/$r/
echo 'watchfrr_options="--netns"' >> /etc/frr/$r/daemons
cp frr/$r.conf /etc/frr/$r/frr.conf
done
sed -i 's/^ospfd=no/ospfd=yes/' /etc/frr/r1/daemons /etc/frr/r2/daemons /etc/frr/r3/daemons
sed -i 's/^bgpd=no/bgpd=yes/' /etc/frr/r3/daemons /etc/frr/r4/daemons
chown -R frr:frr /etc/frr
Apply it and start the four instances. Each frr@ unit starts zebra, mgmtd, staticd and the enabled protocol daemons inside its own namespace, so four instances do not collide.
sudo bash frr-setup.sh
sudo systemctl start frr@r1 frr@r2 frr@r3 frr@r4
Confirm the version from inside one of the instances:
sudo vtysh -N r1 -c "show version" | head -2
The hostname in brackets is the VM’s, not r1. Network namespaces do not get their own hostname, FRR did not apply our hostname r1 line, and the configs saved later carry the VM’s name instead:
FRRouting 10.3 (router01) on Linux(6.12.74+deb13+1-cloud-amd64).
Copyright 1996-2005 Kunihiro Ishiguro, et al.
Give OSPF about 30 seconds, then check r1’s neighbors. Checking too early shows neighbors still in Init or ExStart, because the adjacency needs a few hello and database exchanges to finish. vtysh -N r1 attaches the FRR shell to the r1 instance:
sudo vtysh -N r1 -c "show ip ospf neighbor"
Both neighbors should be Full. If either is missing entirely, the two ends disagree on area, network type or subnet, so compare the interface configs first. The dash after the slash means no DR or BDR role, which is what point-to-point links give you:
Neighbor ID Pri State Up Time Dead Time Address Interface RXmtL RqstL DBsmL
10.255.0.2 1 Full/- 24.363s 35.659s 10.0.12.2 to-r2:10.0.12.1 0 0 0
10.255.0.3 1 Full/- 24.363s 35.681s 10.0.13.2 to-r3:10.0.13.1 0 0 0
A neighbor stuck in Init means hellos only go one way, and ExStart or Exchange usually means an MTU mismatch, covered at the end. With both neighbors full, look at the OSPF routes:
sudo vtysh -N r1 -c "show ip route ospf"
The bracket shows distance and cost. 10.0.20.0/24 sits at [110/30] via r2, and the >* marks it as selected and installed in the kernel. Right after start you may briefly see it via the cost 110 direct link while the r2 to r3 adjacency is still forming; ours settled within 20 seconds:
Codes: K - kernel route, C - connected, L - local, S - static,
R - RIP, O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
f - OpenFabric, t - Table-Direct,
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
t - trapped, o - offload failure
IPv4 unicast VRF default:
O 10.0.10.0/24 [110/10] is directly connected, to-h1, weight 1, 00:00:36
O 10.0.12.0/30 [110/10] is directly connected, to-r2, weight 1, 00:00:36
O 10.0.13.0/30 [110/100] is directly connected, to-r3, weight 1, 00:00:36
O>* 10.0.20.0/24 [110/30] via 10.0.12.2, to-r2, weight 1, 00:00:16
O>* 10.0.23.0/30 [110/20] via 10.0.12.2, to-r2, weight 1, 00:00:21
The terminal capture below is the same pair of commands as they ran on the lab VM, with both neighbors Full and the cost 30 route selected.

You will notice that OSPF also learned 10.0.23.0/30, the transit link we had to add by hand in the static lab, so traceroute from h1 now shows all four hops without any extra route.
Check administrative distance with a floating static
Add a static route for h2’s network through r3 with a distance of 200, higher than OSPF’s 110. FRR accepts the route but does not use it while OSPF still offers the prefix. That is the behavior you want from a backup route, because it must not steer traffic while the primary is healthy.
sudo vtysh -N r1 -c "configure terminal" -c "ip route 10.0.20.0/24 10.0.13.2 200"
FRR now holds two routes for the same prefix and selects by distance. You can read the decision directly from the output, so there is no need to guess which source the router trusts. The OSPF route is marked best; the static one waits behind it:
sudo vtysh -N r1 -c "show ip route 10.0.20.0/24"
Both sources are listed with their distance. The static entry has no asterisk, so it is not in the kernel’s forwarding table.
Routing entry for 10.0.20.0/24
Known via "static", distance 200, metric 0
Last update 00:00:02 ago
10.0.13.2, via to-r3, weight 1
Routing entry for 10.0.20.0/24
Known via "ospf", distance 110, metric 30, best
Last update 00:00:35 ago
* 10.0.12.2, via to-r2, weight 1
If OSPF ever lost 10.0.20.0/24 completely, the static route would move into the kernel. Remove it before the failover tests so the results show OSPF alone:
sudo vtysh -N r1 -c "configure terminal" -c "no ip route 10.0.20.0/24 10.0.13.2 200"
With the static route gone, only OSPF decides where traffic for h2 goes.
Test OSPF reconvergence after a link failure
How long your traffic stops after a failure depends on how the failure is detected, not on SPF math. We ran a ping from h1 to h2 at short intervals, broke the r1 to r2 link three different ways, and read the outage from the ping timestamps:
| Failure | Detected by | Pings lost | Outage |
|---|---|---|---|
| Interface goes down (carrier loss) | Kernel link state, immediately | 0 of 100 at 50 ms | Under 50 ms |
| Link up, all traffic dropped (silent) | OSPF dead interval, 40 s | 195 of 300 sent at 200 ms (105 received) | 39.96 s |
| Same silent failure, BFD enabled | BFD, 3 x 300 ms | 12 of 200 at 50 ms | 0.73 s |
Carrier loss is the easy case. Pulling the interface down tells ospfd at once, so it floods the change and recalculates before the next 50 ms ping. All three tests run the ping as a background job, so run sudo -v first if sudo might ask for your password:
sudo ip netns exec h1 ping -D -i 0.05 -c 100 10.0.20.10 > ping-linkdown.txt & sleep 2; date +%T.%N; sudo ip -n r1 link set to-r2 down; wait
All 100 replies arrived, and the route moved to the direct link at cost 110. That is the best case, because the kernel saw the interface go down and told FRR at once.
--- 10.0.20.10 ping statistics ---
100 packets transmitted, 100 received, 0% packet loss, time 5543ms
rtt min/avg/max/mdev = 0.036/0.053/0.082/0.007 ms
The silent failure is the one that hurts in production: a fiber cut on the far side of a media converter, a switch that keeps the port up but stops forwarding, a provider fault. In all of these the interface stays up, so nothing tells the routing protocol that the path is gone. Bring the link back, then drop every packet on it with netem while leaving the interfaces up:
sudo ip -n r1 link set to-r2 up; sleep 15; sudo vtysh -N r1 -c "show ip ospf neighbor"
Do not start the silent-failure test until 10.255.0.2 is back to Full. Hellos go out every 10 seconds, so the adjacency takes a few seconds to re-form, and until it does, h1’s traffic still runs over the cost 110 direct link and the test measures nothing. In our run it was Full after 15 seconds; if yours is not, run the neighbor command again:
Neighbor ID Pri State Up Time Dead Time Address Interface RXmtL RqstL DBsmL
10.255.0.3 1 Full/- 1m59s 30.448s 10.0.13.2 to-r3:10.0.13.1 0 0 0
10.255.0.2 1 Full/- 5.012s 34.987s 10.0.12.2 to-r2:10.0.12.1 0 0 0
With both neighbors Full, start the ping in the background and break the link three seconds later, so the capture holds a few good replies before the fault:
sudo ip netns exec h1 ping -D -i 0.2 -c 300 10.0.20.10 > ping-silent.txt & sleep 3; date +%T.%N; sudo ip netns exec r1 tc qdisc add dev to-r2 root netem loss 100%; sudo ip netns exec r2 tc qdisc add dev to-r1 root netem loss 100%; wait
Wait for the ping to finish (60 seconds) before reading the result. Nothing tells OSPF the link is broken, so it keeps sending your traffic into it until the 40 second dead timer expires. Meanwhile r1 answers h1 with Destination Host Unreachable, because it cannot resolve r2’s MAC address over the dead link either:
--- 10.0.20.10 ping statistics ---
300 packets transmitted, 105 received, +38 errors, 65% packet loss, time 60968ms
rtt min/avg/max/mdev = 0.039/0.053/0.069/0.004 ms, pipe 16
BFD (Bidirectional Forwarding Detection) fixes this with its own fast hellos, and OSPF tears the adjacency down the moment BFD declares the path dead. It costs a few small packets per second per link, so it is cheap enough to run on every routed link you care about. Remove the netem rules, enable bfdd on r1 and r2, and turn on BFD for the shared link:
sudo ip netns exec r1 tc qdisc del dev to-r2 root; sudo ip netns exec r2 tc qdisc del dev to-r1 root
sudo sed -i "s/^bfdd=no/bfdd=yes/" /etc/frr/r1/daemons /etc/frr/r2/daemons && sudo systemctl restart frr@r1 frr@r2
sudo vtysh -N r1 -c "configure terminal" -c "interface to-r2" -c "ip ospf bfd" -c "end" -c "write memory"
sudo vtysh -N r2 -c "configure terminal" -c "interface to-r1" -c "ip ospf bfd" -c "end" -c "write memory"
Wait for the adjacency to come back (about 40 seconds after the restart), then check the BFD session. It should be up with FRR’s default timers. The restart drops the adjacency, so expect the OSPF neighbor to cycle through its states again before BFD appears.
sudo vtysh -N r1 -c "show bfd peers brief"
You should see one session to r2, in the up state. If the status reads down or init, check that bfdd is enabled on both routers, because BFD needs a peer on each side.
Session count: 1
SessionId LocalAddress PeerAddress Status
========= ============ =========== ======
3654912465 10.0.12.1 10.0.12.2 up
show bfd peer 10.0.12.2 reported 300 ms transmit and receive intervals with a detect multiplier of 3, so BFD gives up after about 900 ms of silence. Repeat the silent failure, this time pinging every 50 ms:
sudo ip netns exec h1 ping -D -i 0.05 -c 200 10.0.20.10 > ping-bfd.txt & sleep 3; date +%T.%N; sudo ip netns exec r1 tc qdisc add dev to-r2 root netem loss 100%; sudo ip netns exec r2 tc qdisc add dev to-r1 root netem loss 100%; wait
Twelve pings went missing at 50 ms intervals. Compare outage times rather than ping counts, because the silent run pinged every 200 ms: the gap fell from 39.96 seconds to 0.73 seconds, roughly the 900 ms BFD detection time.
--- 10.0.20.10 ping statistics ---
200 packets transmitted, 188 received, 6% packet loss, time 11146ms
rtt min/avg/max/mdev = 0.029/0.056/0.080/0.008 ms
The outage figures in the table come from the gap between consecutive replies in each capture. ping -D prefixes every line with a Unix timestamp in brackets, so awk can find any gap longer than half a second:
for f in ping-linkdown.txt ping-silent.txt ping-bfd.txt; do echo "== $f"; awk -F'[][]' '/bytes from/ {t=$2; if (last && t-last > 0.5) printf "outage: %.2f s\n", t-last; last=t}' $f; done
The carrier-loss capture has no gap at all, so you only see its file name.
== ping-linkdown.txt
== ping-silent.txt
outage: 39.96 s
== ping-bfd.txt
outage: 0.73 s
The capture from the lab terminal shows the same three results.

The takeaway for your own network: OSPF on its default timers protects you from failures the interface can see and leaves you exposed for 40 seconds to the ones it cannot. We’d enable BFD on every OSPF link that crosses anything other than a direct cable, rather than shortening the hello and dead timers, because BFD reacts faster and doesn’t load the routing process with extra hellos. Clear the netem rules before moving on:
sudo ip netns exec r1 tc qdisc del dev to-r2 root; sudo ip netns exec r2 tc qdisc del dev to-r1 root
The r1 to r2 link carries traffic again, and BFD stays configured for the rest of the lab.
Set up eBGP with another network
eBGP is how your network swaps routes with one you do not control. Here r3 (AS 65001) peers with r4 (AS 65002), advertises the two host LANs, and learns 198.51.100.0/24 in return. Configure the session on r3 from the FRR shell:
sudo vtysh -N r3 -c "configure terminal" -c "router bgp 65001" -c "bgp router-id 10.255.0.3" -c "neighbor 192.0.2.2 remote-as 65002" -c "address-family ipv4 unicast" -c "network 10.0.10.0/24" -c "network 10.0.20.0/24"
The network statements only advertise prefixes that already exist in r3’s routing table, which is why OSPF had to be running first: 10.0.10.0/24 is an OSPF route on r3. If you skip OSPF and run this on a fresh r3, BGP accepts the command but advertises nothing, so check the RIB when a prefix is missing. Check the session after about 10 seconds:
sudo vtysh -N r3 -c "show bgp summary"
The session is up, since it has an Up/Down time, but no prefixes move in either direction. That combination, an established session with no prefixes, is the signature of a policy problem rather than a connectivity one.
IPv4 Unicast Summary:
BGP router identifier 10.255.0.3, local AS number 65001 VRF default vrf-id 0
BGP table version 2
RIB entries 3, using 384 bytes of memory
Peers 1, using 23 KiB of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
192.0.2.2 4 65002 3 3 2 0 0 00:00:13 (Policy) (Policy) N/A
Total number of neighbors 1
(Policy) is FRR applying RFC 8212, described in the FRR BGP documentation: an eBGP session without an inbound and an outbound policy exchanges nothing. The fix is a real policy, not switching the check off. Prefix lists name exactly what may cross the border, and route maps attach them to the neighbor. On r3, accept only r4’s prefix and send only the two LANs, and redistribute what BGP learns into OSPF so r1 and r2 can reach it:
sudo vtysh -N r3 -c "configure terminal" -c "ip prefix-list FROM-AS65002 seq 5 permit 198.51.100.0/24" -c "ip prefix-list TO-AS65002 seq 5 permit 10.0.10.0/24" -c "ip prefix-list TO-AS65002 seq 10 permit 10.0.20.0/24" -c "route-map AS65002-IN permit 10" -c "match ip address prefix-list FROM-AS65002" -c "exit" -c "route-map AS65002-OUT permit 10" -c "match ip address prefix-list TO-AS65002" -c "exit" -c "router bgp 65001" -c "address-family ipv4 unicast" -c "neighbor 192.0.2.2 route-map AS65002-IN in" -c "neighbor 192.0.2.2 route-map AS65002-OUT out" -c "exit-address-family" -c "exit" -c "router ospf" -c "redistribute bgp" -c "end" -c "write memory"
r4 needs the mirror image, since each side enforces its own policy and a one-sided fix still leaves the other direction empty. Policy is local to each router, so you have to configure both ends.
sudo vtysh -N r4 -c "configure terminal" -c "ip prefix-list FROM-AS65001 seq 5 permit 10.0.10.0/24" -c "ip prefix-list FROM-AS65001 seq 10 permit 10.0.20.0/24" -c "ip prefix-list TO-AS65001 seq 5 permit 198.51.100.0/24" -c "route-map AS65001-IN permit 10" -c "match ip address prefix-list FROM-AS65001" -c "exit" -c "route-map AS65001-OUT permit 10" -c "match ip address prefix-list TO-AS65001" -c "exit" -c "router bgp 65002" -c "address-family ipv4 unicast" -c "neighbor 192.0.2.1 route-map AS65001-IN in" -c "neighbor 192.0.2.1 route-map AS65001-OUT out" -c "end" -c "write memory"
Check the summary again. Give it a few seconds, because BGP only sends an update after the policy change triggers a refresh.
sudo vtysh -N r3 -c "show bgp summary"
The counters replace (Policy): one prefix received, two sent:
IPv4 Unicast Summary:
BGP router identifier 10.255.0.3, local AS number 65001 VRF default vrf-id 0
BGP table version 3
RIB entries 5, using 640 bytes of memory
Peers 1, using 23 KiB of memory
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd PfxSnt Desc
192.0.2.2 4 65002 13 13 3 0 0 00:00:54 1 2 N/A
Total number of neighbors 1
The BGP table shows where each path came from. r3’s own networks have next hop 0.0.0.0 and no AS path, while 198.51.100.0/24 arrived from 192.0.2.2 with AS path 65002:
sudo vtysh -N r3 -c "show bgp ipv4 unicast"
Every entry is valid and best (*>), because there is only one path to each prefix. With more than one neighbor you would see several paths per prefix, and BGP would pick one by its attributes.
BGP table version is 3, local router ID is 10.255.0.3, vrf id 0
Default local pref 100, local AS 65001
Status codes: s suppressed, d damped, h history, u unsorted, * 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
*> 10.0.10.0/24 0.0.0.0 0 32768 i
*> 10.0.20.0/24 0.0.0.0 0 32768 i
*> 198.51.100.0/24 192.0.2.2 0 0 65002 i
Displayed 3 routes and 3 total paths
On r4, the routes from AS 65001 show up as B>* with distance 20, eBGP’s default. That value only matters on r4, because it decides whether BGP or another source wins for the same prefix.
sudo vtysh -N r4 -c "show ip route bgp"
Both LANs point back at r3, the only way into AS 65001. r4 has no other route into AS 65001, so a broken session would make both LANs unreachable from it.
Codes: K - kernel route, C - connected, L - local, S - static,
R - RIP, O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
T - Table, v - VNC, V - VNC-Direct, A - Babel, F - PBR,
f - OpenFabric, t - Table-Direct,
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
t - trapped, o - offload failure
IPv4 unicast VRF default:
B>* 10.0.10.0/24 [20/0] via 192.0.2.1, to-r3, weight 1, 00:00:20
B>* 10.0.20.0/24 [20/0] via 192.0.2.1, to-r3, weight 1, 00:00:20
Inside AS 65001, you will find that r1 learns the external prefix through OSPF, because r3 redistributes it. Redistributed routes enter OSPF as external routes with a default metric of 20:
sudo vtysh -N r1 -c "show ip route 198.51.100.0/24"
r1 uses the cost 20 path through r2 to reach r3, the router that injected it. Traffic for the external network therefore follows the same internal path as traffic for h2.
Routing entry for 198.51.100.0/24
Known via "ospf", distance 110, metric 20, best
Last update 00:00:27 ago
* 10.0.12.2, via to-r2, weight 1
The end-to-end test crosses both protocols, OSPF inside AS 65001 and BGP across the border. If it fails, the hop where the stars start tells you which protocol to check.
sudo ip netns exec h1 traceroute -n 198.51.100.1
Four hops: r1, r2, r3, then r4’s address in the other network:
traceroute to 198.51.100.1 (198.51.100.1), 30 hops max, 60 byte packets
1 10.0.10.1 0.034 ms 0.003 ms 0.003 ms
2 10.0.12.2 0.016 ms 0.005 ms 0.006 ms
3 10.0.23.2 0.015 ms 0.006 ms 0.006 ms
4 198.51.100.1 0.031 ms 0.009 ms 0.007 ms
On a real edge router you would add more policy on top: a maximum-prefix limit on the session, filters for bogons and your own prefixes on the inbound side, and MD5 session authentication, which FRR sets with a line such as neighbor 192.0.2.2 password Lab-Secret-1. For a two-router lab, the prefix lists above are the part that teaches how BGP thinks. When you’re done, stop the FRR instances and delete the namespaces:
sudo systemctl stop frr@r1 frr@r2 frr@r3 frr@r4 && sudo bash teardown-lab.sh
The configs and daemons files in /etc/frr/r1 to /etc/frr/r4 stay on disk; the routes do not. To bring the lab back, rebuild the topology, re-run the forwarding loop (the new namespaces start with the host’s value again), and start the four frr@ services. If you want the stock FRR service back afterwards, sudo systemctl enable --now frr reverses what the setup script disabled; we did not run that step in the lab. If you’d rather practice the same protocols with Cisco images in a graphical simulator, our guide to installing GNS3 on Debian runs on the same kind of VM.
Troubleshoot routing in the lab
Every error below happened while building this lab, and each fix was confirmed by re-running the failing command, so you can reproduce any of them on your own copy. Most routing faults come down to one of three things: a router that won’t forward, a missing route in one direction, or two neighbors that disagree on a setting.
RTNETLINK answers: File exists
Our first build named the host-side interfaces eth0, and the script stopped at the first link with RTNETLINK answers: File exists. ip link add checks the requested name against the namespace it runs in before creating the pair, so the name collided with the VM’s own eth0. Pick names you do not already use in the root namespace (we used to-r1 and to-r3), delete the half-built namespaces with the teardown script, and build again.
Ping to the gateway works, but nothing past it
With static routes in place and forwarding still off, h1 could ping r1 but nothing beyond it, and traceroute printed only stars. Nothing in the output pointed at the cause, so the next step was to ping each router address in turn.
sudo ip netns exec h1 ping -c 3 -W 1 10.0.20.10
All three pings timed out, with no error message to explain why. A router that drops traffic silently is the classic sign of disabled forwarding, so test the gateway itself next.
PING 10.0.20.10 (10.0.20.10) 56(84) bytes of data.
--- 10.0.20.10 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2044ms
r1’s own address on the r2 link, 10.0.12.1, answered normally, so h1 could reach the router but not get through it. That isolates the problem to forwarding rather than addressing or routes. sudo ip netns exec r1 sysctl net.ipv4.ip_forward returned net.ipv4.ip_forward = 0, and setting it to 1 on every router fixed it. On a physical Linux router the same symptom shows up after a reboot when the setting was only applied with sysctl -w and never written to /etc/sysctl.d/.
Traceroute shows * for one hop, but ping works
With the four static routes but no route to the 10.0.23.0/30 transit link, ping succeeded and traceroute kept losing hop 3. Because ping worked, the forward and return paths for h1 and h2 were fine, which narrowed it to the router’s own replies.
sudo ip netns exec h1 traceroute -n -q 1 -f 3 -m 3 10.0.20.10
r3 was not the problem, even though its hop is the one showing a star. A star means the reply never reached h1, so the fault can be anywhere on the return path.
traceroute to 10.0.20.10 (10.0.20.10), 3 hops max, 60 byte packets
3 *
A capture with tcpdump inside r3 (install the tcpdump package to repeat it) showed it sending the ICMP time exceeded reply from 10.0.23.2. The reply died on r1, which had no route back to 10.0.23.2, and the namespaces had inherited loose reverse path filtering (rp_filter = 2) from the Debian host’s /usr/lib/sysctl.d/50-default.conf. The kernel default is 0. Loose mode drops packets from sources with no route at all, and the kernel counts every drop:
sudo ip netns exec r1 nstat -az TcpExtIPReversePathFilter
The counter went from 14 to 15 with each single-probe traceroute, one drop per lost reply. If your counter stays flat, rp_filter is not the cause and you should look at firewall rules instead.
#kernel
TcpExtIPReversePathFilter 15 0.0
Adding 10.0.23.0/30 via 10.0.12.2 on r1 brought hop 3 back. With OSPF this never happens, because transit links are advertised like any other prefix. Before turning rp_filter off to make a symptom go away, check whether the router is simply missing a route.
OSPF neighbor stuck in ExStart
We set the r2 side of the r2 to r3 link to an MTU of 1400 and left r3 at 1500. The adjacency came up to ExStart and stayed there:
sudo vtysh -N r3 -c "show ip ospf neighbor"
On r3, the neighbor r1 stayed Full while r2 sat in ExStart. Only the link with the MTU mismatch was affected, so the rest of the area kept routing around it.
Neighbor ID Pri State Up Time Dead Time Address Interface RXmtL RqstL DBsmL
10.255.0.1 1 Full/- 4m36s 34.870s 10.0.13.1 to-r1:10.0.13.2 0 0 0
10.255.0.2 1 ExStart/- 31.833s 38.168s 10.0.23.1 to-r2:10.0.23.2 0 0 0
r2’s ospfd logged the reason every 5 seconds. Search the journal for it:
sudo journalctl -t ospfd --since "-2min" --no-pager | grep -i mtu | tail -3
The message names both MTUs. That tells you which side to change, so you do not have to guess which end is wrong.
Oct 20 10:17:45 router01 ospfd[12566]: [XZZ9Y-NNTMQ][EC 134217741] Packet[DD]: Neighbor 10.255.0.3 MTU 1500 is larger than [to-r3:10.0.23.1]'s MTU 1400
Oct 20 10:17:50 router01 ospfd[12566]: [XZZ9Y-NNTMQ][EC 134217741] Packet[DD]: Neighbor 10.255.0.3 MTU 1500 is larger than [to-r3:10.0.23.1]'s MTU 1400
Oct 20 10:17:55 router01 ospfd[12566]: [XZZ9Y-NNTMQ][EC 134217741] Packet[DD]: Neighbor 10.255.0.3 MTU 1500 is larger than [to-r3:10.0.23.1]'s MTU 1400
OSPF refuses a database description packet that announces a larger MTU than the receiving interface supports. Your adjacency therefore never gets past ExStart. We set r2’s side back to 1500 with sudo ip -n r2 link set to-r3 mtu 1500, and the neighbor was Full again within seconds. ip ospf mtu-ignore also makes the error go away, but it hides a real mismatch that drops large packets later. Fix the MTU instead.
BGP summary shows (Policy) instead of a prefix count
The session establishes, but nothing is sent or received. Both columns read (Policy), as in the BGP section above. What you are seeing is FRR’s RFC 8212 default for eBGP in the traditional profile, not a broken session. Attach an inbound and an outbound route map to the neighbor on both routers, as shown in the BGP section. no bgp ebgp-requires-policy turns the check off. We’d only accept that on a disposable lab router, because on a real border it lets a misconfigured neighbor send you the whole internet table.