Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Peer templates** (#20). A template is a peer without an identity: the role,
policy chains, import limit, transport safeguards and export transforms a kind
of session should have, kept once under Peers → Templates. Link any number of
peers to it and they take that shape; **save the template and every linked peer
is rewritten** in one transaction, then reviewed on the Changes page like any
other edit. Each peer keeps its own name, neighbor, AS, password and the
enabled/drain switches. The peer form shows inherited values greyed out with the
template named; choosing *None* detaches the peer and leaves its values in
place. *Save as template* on a peer captures its shape (and can link the peer in
the same step); *Add peer* on a template's row starts a session already linked.
A template cannot be deleted while peers link to it, and a policy cannot be
deleted while a template chains it. In `bird.conf` a template in use renders as
BIRD's own `template bgp NAME { … }` carrying the shared session options, and
each linked peer is declared `protocol bgp X from NAME` — its filters, channel
and identity stay in its own block. Unlinked peers render byte-for-byte as
before. (Schema version 38: two new tables and two nullable/defaulted columns
on `peers`; existing peers are untouched.)
- A linked peer can keep **its own import limit** (*Use this peer's own import
limit*), for the one IX peer that sends far more than the rest; everything else
still follows the template.
- **Attach or detach many peers at once** from the peers list, the migration path
for peers configured one by one before templates existed.
- **Import from BIRD** offers a template per row, so adopted sessions arrive with
the template's role, chains, limit and safeguards instead of bare identities.
- Lint findings that are identical across peers of one template **fold into one
line** attributed to the template.

### Changed
- The peer names `new`, `seed`, `preview`, `templates` and `attach` are refused:
each is a page under `/peers/` that the router serves before the peer-name
wildcard, so a peer so named could be created but never opened.

### Fixed
- The peer form's role-specific checkbox group for the *other* role (the iBGP
switches on an eBGP peer, and vice versa) was visible, greyed out, instead of
hidden: the group's flex layout overrode the `hidden` attribute.

## [0.5.0] - 2026-07-22

### Added
Expand Down
7 changes: 4 additions & 3 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
> - **Alerting** grew far past "one webhook, no SMTP": Slack, Discord, SMTP email and generic
> webhooks, multiple destinations with per-kind filtering and cooldown, plus BIRD-unreachable,
> config-drift and IRR-refresh alerts.
> - The **data model** below lists `communities`, `functions` and `peer_templates` tables that
> were never built as tables (communities became inline value parsing; peer templates became
> "clone a peer"). Several shipped features — BMP, IRR/bgpq4 expansion and auto-refresh, RTBH,
> - The **data model** below lists `communities` and `functions` tables that were never
> built as tables (communities became inline value parsing). `peer_templates` was
> first replaced by "clone a peer" and then built for real, though as a model-level
> link rather than BIRD `template bgp` blocks. Several shipped features — BMP, IRR/bgpq4 expansion and auto-refresh, RTBH,
> BFD, GTSM, graceful restart, route-history sampling, the split `birdy.d/` layout, and
> seed-from-BIRD — are not mentioned here at all.

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ Writing `bird.conf` is still a deliberate act you take in the UI, not something
<td width="50%"><img src="docs/screenshots/changes-dark.png" alt="Review and apply a candidate config"><br><sub><b>Syntax-check, lint, diff, then apply with an armed auto-revert.</b></sub></td>
</tr>
<tr>
<td width="50%"><img src="docs/screenshots/peer-templates.png" alt="Peer templates: the shape of a session, kept once and linked from many peers"><br><sub><b>Peer templates — thirty IX peers change in one edit.</b></sub></td>
<td width="50%"><img src="docs/screenshots/peer-form-linked.png" alt="A peer linked to a template, inheriting its chains, limit and safeguards"><br><sub><b>A linked peer: identity is its own, the shape comes from the template.</b></sub></td>
</tr>
<tr>
<td width="50%"><img src="docs/screenshots/policies.png" alt="Composable import and export policies"><br><sub><b>Composable policy chains — import rejects, export accepts.</b></sub></td>
<td width="50%" valign="top"><br>

Expand Down Expand Up @@ -246,6 +250,10 @@ things follow from that, and both are one setting away:
drain (RFC 8326 graceful shutdown), and BFD per peer
- Composable import and export policy chains that can match communities, rather than one policy per
session; clone a peer to make another of the same shape
- **Peer templates** — the shape of a session (chains, limits, safeguards, transforms) kept once and
linked from any number of peers; save the template and every linked peer is rewritten, reviewed on
the Changes page like any other edit. Rendered as BIRD's own `template bgp` with each linked peer
declared `from` it
- A library of prefix sets, AS sets, and static routes — both set kinds can be expanded from an IRR
AS-SET with `bgpq4` (used automatically when installed), and kept current on a schedule (never
auto-applied)
Expand Down
63 changes: 60 additions & 3 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,66 @@ apply to the role you pick.
| **Graceful restart** | all | Negotiate BGP graceful restart so forwarding continues across a control-plane restart on either end: **aware** (help a restarting neighbour — BIRD's default), **on** (negotiate in both directions), or **off** (drop routes immediately). |
| **Import / export policy chains** | eBGP | Ordered lists of policies. **Imports compose with AND** (a route must survive every import policy); **exports compose with OR** (a route is announced if any export policy permits it). With no export policy the session is receive-only (RFC 8212 default-deny). |

**Clone a peer** to use one as a template: birdy copies the role, policy chains,
limits and transforms, and drops only the identity (name, addresses, ASN) and the
password.
**Clone a peer** to make another of the same shape: birdy copies the role, policy
chains, limits and transforms, and drops only the identity (name, addresses, ASN)
and the password. A clone of a peer linked to a template is linked to the same
template.

### Peer templates

Thirty peers at an exchange usually want the same thing: the same import and export
chains, the same limit, the same safeguards. A **peer template** (Peers → Templates)
is that shape kept once — a peer without an identity — and any number of peers can
**link** to it. Save the template and **every linked peer is rewritten** in the same
transaction; the Changes page then shows each of them as a changed section, behind
the usual syntax check, lint and armed auto-revert, so nothing reaches the router
until you have looked.

| | |
|---|---|
| **The template owns** | Role, the import and export chains, import limit and action, import/export communities, AS-path prepend, require-first-AS, origin-peer-only, RFC 9234 role, GTSM, BFD, graceful restart, passive, multihop, next-hop-self, route-reflector client, iBGP export fallback. |
| **Each peer keeps** | Name, description, neighbor address, remote AS, local address, interface, tunnel endpoint, MD5 password, and the **Enabled** and **Drain** switches. |

- **Link a peer** from the *Template* field at the top of its form. The governed
controls fill with the template's values and grey out; the live preview shows the
result. Whatever the form posts for a governed field is ignored on save — the
template wins.
- **Keep one peer's own import limit.** The one thing that genuinely differs between
thirty otherwise identical IX peers is how much the big one sends. Tick *Use this
peer's own import limit* on a linked peer and its limit and action stay its own
through every template save; untick it and the template's limit returns. Nothing
else can be overridden — a peer that needs a different chain needs a different
template.
- **Detach** by choosing *None*. The peer keeps the values it inherited and stops
following the template — the same result as a clone, in place.
- **Capture an existing peer** with *Save as template* on its edit page: the new
template starts from that peer's shape, and the peer can be linked to it in the
same step. From a template's row, *Add peer* starts a new session already linked,
so only the identity is left to type.
- **Attach many at once.** Tick peers on the peers list and use the bar under the
table to attach them all to one template (or detach them). Attaching replaces each
peer's chains, limit and safeguards with the template's — it is how a router whose
peers were configured one by one moves onto templates.
- **Import from BIRD already linked.** The *Import from BIRD* page offers a template
per row (and one control to set every checked row); an imported session linked to a
template arrives with its role, chains, limit and safeguards, which a plain import
cannot know from the socket.
- A template **cannot be deleted while peers link to it**, and a policy cannot be
deleted while a template chains it. A linked peer's protocol block in the rendered
config carries a comment naming its template. On the Changes page, identical lint
findings about peers of one template fold into a single line attributed to the
template — thirty peers missing an import limit is one thing to fix, once.

In the rendered config a template is BIRD's own `template bgp NAME { … }`, carrying the
session options every linked peer shares — multihop, passive, BFD, GTSM, graceful restart,
the RFC 9234 role, route reflection — and each linked peer is declared
`protocol bgp NAME from TEMPLATE { … }`. What stays in the peer's own block is everything
peers differ in: the neighbor and password, the filters (they embed the peer's own ASN and
transforms), and the whole channel — its address family follows the neighbor, and its
import limit is the one thing a linked peer may override. A template nobody links to is
not written at all, so creating one changes nothing until a peer uses it. In the split
`birdy.d/` layout the template blocks are filed with the policies (`08-templates-*.conf`),
ahead of the peers that inherit them.

---

Expand Down
Binary file added docs/screenshots/peer-form-linked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/peer-templates.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions internal/render/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var groupNum = map[string]string{
"protocols": "06",
"static": "07",
"policies": "08",
"templates": "08", // with the policies: what peers are declared "from" comes before them
"peers": "09",
"raw": "10",
}
Expand Down
56 changes: 56 additions & 0 deletions internal/render/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,5 +356,61 @@ func Lint(in Input) []Warning {
if in.LocalASN == 0 || in.RouterID == "" {
add(SeverityWarn, "", "Set the router ID and local ASN before applying anything.")
}
return foldByTemplate(out, in.Peers)
}

// foldByTemplate collapses identical findings about peers linked to the same
// template into one, attributed to the template. Thirty IX peers on a template
// with no import limit is one problem, not thirty lines burying everything
// else on the Changes page — and the fix is in one place, the template. A
// finding that names something peer-specific (an ASN, a neighbor) differs
// from peer to peer and stays where it is; so does a finding only one linked
// peer has. Order is kept: a folded finding sits where its first member was.
func foldByTemplate(ws []Warning, peers []store.Peer) []Warning {
templateOf := map[string]string{}
for _, p := range peers {
if p.TemplateName != "" {
templateOf[p.Name] = p.TemplateName
}
}
if len(templateOf) == 0 {
return ws
}
type key struct{ template, severity, message string }
members := map[key][]int{}
for i, w := range ws {
if t := templateOf[w.Peer]; t != "" {
k := key{t, w.Severity, w.Message}
members[k] = append(members[k], i)
}
}
folded := map[int]Warning{} // first member's index -> the one finding that replaces the group
drop := map[int]bool{}
for k, idx := range members {
if len(idx) < 2 {
continue
}
folded[idx[0]] = Warning{
Severity: k.severity,
Peer: fmt.Sprintf("%s (%d peers)", k.template, len(idx)),
Message: k.message,
}
for _, i := range idx[1:] {
drop[i] = true
}
}
if len(folded) == 0 {
return ws
}
out := make([]Warning, 0, len(ws))
for i, w := range ws {
if drop[i] {
continue
}
if f, ok := folded[i]; ok {
w = f
}
out = append(out, w)
}
return out
}
Loading