When both ends dial each other, exactly one TCP connection must survive
and both ends must pick the same one. Previously ferrix relied on the
other router to sort this out and could briefly hold two sessions.
- collision: a shared map keyed by the peer's BGP Identifier holds the one
connection currently preferred for that peer plus a kill signal. Each
connection calls admit() on receiving the peer's OPEN. The connection
initiated by the higher BGP Identifier wins; the loser gets Cease /
Connection Collision Resolution, whether it is the new connection or an
existing one in OpenConfirm running in another task.
- A collision with an Established session closes the new connection (RFC
default). The optional CollisionDetectEstablishedState is available as
collision_detect_established_state: a restarted peer's new connection
then replaces the stale session at once instead of being refused until
the hold timer expires. Enabled in the lab config.
- Two connections opened by the same side: the newer wins.
- OPEN validation rejects a peer using our own BGP Identifier or 0.0.0.0
with Bad BGP Identifier; collision resolution needs distinct identifiers.
- OpenConfirm no longer times out instantly when the negotiated hold time
is zero.
- Sessions record which side opened them; shown as "opened by" on the
status page and as `direction` in /api/peers. New counter
bgp_connection_collisions_total{closed}.
- Tests: unit tests for the rule (including that both ends agree), and
integration tests for simultaneous open in both identifier orders and
both arrival orders, Established default, restart replacement with route
flush, distinct identifiers, slot release, and Bad BGP Identifier.
Existing fan-out test now gives its two peers distinct identifiers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4.1 KiB
BGP teaching lab
A small topology in which a prefix announced from the ferrix_bgp web UI is carried around a loop of two routers and arrives back, so students can watch what their path attributes and communities do.
announce re-advertise
┌─────────┐ ──────▶ ┌────────┐ ──────▶ ┌────────┐
│ ferrix │ │ r1 │ │ r2 │
│ AS65000 │ │ AS65001│ │ AS65002│
│ web UI │ ◀──────────────────────────────────────┘
└─────────┘ comes back with AS path 65002 65001 65000
- ferrix (AS65000,
10.200.0.1) dials r1 and r2 and announces whatever students enter onhttp://localhost:8080/statusz. It does not forward routes it learns, so the only routes it sends are the students'. - r1 (AS65001, FRR) applies policy to routes from ferrix based on the
communities they carry, then passes them to r2. It originates
192.0.2.0/24. - r2 (AS65002, FRR) refuses routes directly from ferrix, so it only learns
student prefixes via r1, and re-advertises everything to ferrix. It
originates
198.51.100.0/24.
Running it
From the repository root:
docker compose -f docs/lab/docker-compose.yml up --build
Then open http://localhost:8080/statusz. Both peers should show
Established within a few seconds and the received table should already
list the two prefixes r1 and r2 originate.
Useful on the routers:
docker exec r1 vtysh -c 'show bgp ipv4 unicast'
docker exec r2 vtysh -c 'show bgp ipv4 unicast 10.100.1.0/24'
docker exec r1 vtysh -c 'show bgp neighbors 10.200.0.1 advertised-routes'
Policy on r1
| Students set | r1 does | Visible effect in ferrix |
|---|---|---|
| nothing | forwards to r2 | prefix returns, AS path 65002 65001 65000 |
NO_EXPORT |
keeps it, does not send to r2 (RFC 1997) | prefix does not return |
65001:100 |
prepends 65001 three times |
returns with path 65002 65001 65001 65001 65001 65000 |
BLACKHOLE (65535:666) |
drops it on import | prefix does not return |
| AS-path prepends on ferrix | nothing special | returns with extra 65000s in the path |
Without containers
fake_peer in crates/integration-tests/examples is enough for the core
loop on one machine: it echoes every prefix back with its own ASN prepended
and honours NO_EXPORT. See bin/bgp/README.md.
Caveats
These container files were written without a container runtime to hand, so they have not been executed. Things most likely to need a touch:
- the FRR image tag, and FRR's route-map / community-list syntax if your version differs from 10.x;
no bgp ebgp-requires-policyis required on FRR 7.4+ or eBGP routes are silently dropped; it is present in both configs;- ferrix binds port 179 inside its container, which requires no special
capability there. If you run it on the host instead, either run as root,
grant
CAP_NET_BIND_SERVICE, or changelisten_endpointsand the FRR neighbor port. - Both ferrix and the FRR routers dial each other, so connection collisions (RFC 4271 §6.8) are normal at start-up: one of the two TCP connections is closed with a Cease / Connection Collision Resolution NOTIFICATION and the other carries the session. ferrix has the lowest BGP Identifier here, so the connection the FRR router opened is the one that survives; the "opened by" column on the status page shows which side that was.
- ferrix does not check AS paths for its own ASN. That is what lets the looped route be accepted; a production BGP speaker would reject it.