Files
ferrix/docs/lab/README.md
T
Rayhaan JaufeerallyandClaude Fable 5.1 87f96f9389 feature: RFC 4271 §6.8 connection collision detection
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>
2026-09-19 15:05:13 +02:00

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 on http://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-policy is 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 change listen_endpoints and 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.