Skip to content

feat: per-session BFD timers - #28

Merged
floreabogdan merged 2 commits into
mainfrom
feat/bfd-timers
Sep 30, 2026
Merged

floreabogdan merged 2 commits into
mainfrom
feat/bfd-timers

Conversation

@floreabogdan

Copy link
Copy Markdown
Owner

Stacked on #21 (peer templates). The base is feat/peer-templates, so this diff shows only the BFD change. Merge #21 first, then retarget this PR to main. Because the repo squash-merges, it will also need a rebase onto #21's squash commit.

Why

A peer with BFD on renders a bare bfd;, and birdy emits a single empty protocol bfd bfd1 {}. Every session therefore runs BIRD's default of 100 ms × 5 and is declared dead after 0.5 s of silence.

That suits a direct link but not iBGP carried over GRE across the internet. On rt1/rt2 (AS210622), the tunnel sessions to each other and to pdxnet flapped several times a day. The path to pdxnet showed about 0.4–0.5 % loss, in bursts of up to 300 ms, with 20-minute 10 pps probes. BFD timed out, BGP sent a Cease, and routes were withdrawn for about a minute each time.

The raw-config escape hatch can't fix this. BIRD hands each BFD request to the first BFD protocol that accepts it (bfd_submit_request() in proto/bfd/bfd.c), and the generated bfd1 accepts everything, so a second protocol in raw config never gets the sessions.

What

  • Peers and templates gain BFD interval (ms) and BFD multiplier. They're rendered on the session alone as

    	bfd {
    		interval 300 ms;
    		multiplier 10;
    	};
    

    using per-peer BFD session options (BIRD 2.0.8+). A tunnel can run 300 ms × 10 while a direct peer beside it keeps the fast default.

  • With both left at 0, the session still renders a bare bfd;, byte-identical to before. Existing configs don't change on upgrade.

  • Both are template-governed shape fields: ApplyTo/TemplateFromPeer, updatePeerShape, template CRUD, the template form JSON (bfdInterval/bfdMultiplier), and TestEveryPeerFieldIsClassified. A template save rewrites the timers on every linked peer, and a template's template bgp block carries them.

  • Validation: interval 10–10000 ms, multiplier 1–255, 0 = BIRD's default. Both are cleared when BFD is off, following validateShape's "normalise rather than reject".

  • UI: two fields under the session checkboxes (governed, and locked when inherited), and the templates list chip shows e.g. BFD 300 ms ×10.

  • Schema version 39: two defaulted columns each on peers and peer_templates.

  • Docs: USAGE (option table, template-owned fields, and the raw-config section, which pointed at "BFD tuning") plus CHANGELOG.

Tests (written first, each watched failing)

  • render: TestBFDTimersRender covers the timer block, a single timer, and the bare bfd; fallback.
  • store: round-trip, validation and normalisation, a template → link → template-save rewrite, plus the new fields in TestEveryPeerFieldIsClassified.
  • web: form → store → edit form; an out-of-range value re-renders with its error; template JSON; templates chip and preview.
  • integration: TestIntegrationBFDTimersParseInBird renders a template with timers, a peer linked to it and a standalone peer with its own timers, then runs bird -p. A mutation check confirmed it fails on bad syntax (syntax error, unexpected '{').

Verification

GitHub Actions currently won't start jobs ("account is locked due to a billing issue"), so this was verified locally with the same steps as ci.yml, on go1.26.8 with GOTOOLCHAIN=local: gofmt clean, go vet (including -tags integration) ok, golangci-lint v2.1.6 0 issues, go test -race ./... ok, govulncheck with no reachable vulnerabilities, CGO_ENABLED=0 build ok, and the integration tests against BIRD 2.14 (Ubuntu 24.04's bird2, the same package CI installs) pass. The bfd { … } syntax also parses on BIRD 2.17.1 (checked with bird -p on rt2).

I didn't click through the UI in a browser. The two inputs reuse the existing field-row/field markup and the generic governed-field fill in peer-form.js.

🤖 Generated with Claude Code

floreabogdan and others added 2 commits September 30, 2026 17:19
A peer with BFD on rendered a bare `bfd;` and one empty `protocol bfd`,
so every session ran BIRD's 100 ms x 5 default: dead after 0.5 s of
silence. That suits a direct link, but iBGP carried over GRE across the
internet flapped several times a day on sub-second loss. The raw-config
escape hatch could not help: BIRD hands each BFD request to the first
BFD protocol that accepts it, and the generated bfd1 accepts everything.

Peers and templates gain an interval (ms) and a multiplier, rendered on
the session alone as `bfd { interval N ms; multiplier M; };` (per-peer
BFD options, BIRD 2.0.8+), so a tunnel can run 300 ms x 10 while a
direct peer beside it keeps the fast default. Both are template-governed
shape fields, validated (10-10000 ms, 1-255; 0 keeps BIRD's default) and
cleared when BFD is off. With both at 0 the session renders a bare
`bfd;`, byte-identical to before. Schema version 39 adds two defaulted
columns each to peers and peer_templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- schema.go: peer_templates declares bfd_interval/bfd_multiplier, as its
  "columns mirror peers" contract requires; v39 still adds them to
  existing databases.
- The interval hint, comment and validation message said 0 means
  "100 ms". BIRD's defaults are send every 100 ms, accept down to 10 ms,
  and `interval` sets both, so say that.
- Settings > Raw configuration still offered BFD tuning, with a
  `protocol bfd` placeholder that can never see a BGP session because
  the generated bfd1 takes every request. Point at the peer form instead.
- USAGE: timers need BIRD 2.0.8+, like RFC 9234 roles; the raw-config
  note no longer reads as if birdy does not validate the timers; note
  that BIRD shares one BFD session per neighbor address.
- Tests: the template block carries the timers and a linked peer does
  not repeat them (a unit test, not only integration), and a v38
  database with BFD peers and templates migrates to zero timers and can
  then store them (checked failing with the v39 step disabled).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@floreabogdan
floreabogdan changed the base branch from feat/peer-templates to main September 30, 2026 14:23
@floreabogdan

Copy link
Copy Markdown
Owner Author

Review follow-up (pushed; rebased onto main after #21 was squash-merged, base retargeted to main):

  • schema.go: peer_templates now declares bfd_interval/bfd_multiplier, per its "columns mirror peers" contract.
  • The hint, comment and validation message said 0 meant "100 ms". BIRD's defaults are send every 100 ms and accept down to 10 ms, and interval sets both; they now say so.
  • Settings → Raw configuration no longer offers BFD tuning. Its protocol bfd placeholder could never see a BGP session, because the generated bfd1 takes every request.
  • USAGE: timers need BIRD 2.0.8+; the raw-config paragraph is reworded so "birdy understands none of it" no longer reads as covering the timers; notes that BIRD shares one BFD session per neighbor address.
  • New tests: a unit test for the template block carrying timers without the linked peer repeating them, and a v38→v39 migration test (mutation-checked with the v39 step disabled).

Not changed, by design: the timers stay template-governed with no per-peer override (as bfd and multihop already are); timers are cleared when BFD is off (validateShape's normalise-rather-than-reject); formInt parsing matches every other numeric field.

Re-verified locally on main + this branch (Actions still billing-locked): gofmt, vet (incl. -tags integration), golangci-lint, go test -race ./..., govulncheck, build, BIRD 2.14 integration (both tests), all green.

@floreabogdan
floreabogdan merged commit 67e2f52 into main Sep 30, 2026
@floreabogdan
floreabogdan deleted the feat/bfd-timers branch September 30, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant