A web UI for BIRD 2.x that runs on your router.
eBGP/iBGP sessions, import/export policy, RPKI, RTBH, BFD — modelled in a database,
rendered to bird.conf, and applied with an armed auto-revert. Plus live visibility
into what every session is actually doing.
One router is managed locally. Multiple Birdy routers can be observed safely.
birdy is a single Go binary you run on a BIRD router, under systemd or in a container. It gives you:
- a live dashboard of every BGP session, read straight from BIRD's control socket, and
- a model of the config — peers, policies, prefix/AS sets — that it renders into the
whole
bird.confand can apply for you, safely, with a one-command rollback.
It works the moment you install it. No flags to discover, no unit to edit: birdy detects what
the router can do — bgpq4 for IRR expansion, ping/traceroute for diagnostics — and enables it.
Writing bird.conf is still a deliberate act you take in the UI, not something it does on install.
Warning
birdy is beta software. Expect bugs. It is a personal project released in the hope it is useful to someone else. Nothing here has been through the kind of testing a piece of routing infrastructure deserves.
Caution
Do not point birdy at a router with a configuration you care about. birdy does not import,
merge with, or preserve an existing bird.conf. It renders the entire config file from its own
database. Anything it does not know about — a protocol, a filter, a table, a hand-tuned option —
does not exist as far as birdy is concerned, and would be gone from any config it wrote. Use it on
a new router, or on one whose config you are content to re-create inside birdy from scratch.
birdy is opinionated. It does not expose every knob BIRD has. It renders what its authors believe
is good practice — RFC 8212 default-deny on export, bogon prefix and ASN filtering, large communities
to tag route origin, enforce-first-AS on eBGP, next-hop-self on iBGP, RPKI invalid drop — and it will
happily refuse to render a config it thinks is a route leak. Anything it does not model goes in a raw
block, appended verbatim. If you disagree with those opinions, birdy is the wrong tool and you should
write bird.conf by hand. That is a perfectly good way to run a router.
There is no support. No warranty, no SLA, no guarantee of fitness for anything. Issues and pull requests are welcome and may be ignored. If you run this and it breaks your BGP session, your transit, your customers, or your night's sleep, that is entirely your responsibility. You accepted that the moment you ran it. See LICENSE.
📖 Full guide:
docs/USAGE.mdwalks through installing every dependency, each command-line flag, and what every peer and policy knob does.
birdy runs on the router, next to BIRD. Pick one of these.
Download a binary
Grab the archive for your platform from the latest release (linux amd64/arm64/arm, freebsd, macOS), verify it, and drop the binary on the router:
tar -xzf birdy_*_linux_amd64.tar.gz
sudo install birdy /usr/local/bin/birdyLinux package (.deb / .rpm / .apk)
Each release ships packages for amd64, arm64 and armhf. They install the binary to
/usr/bin/birdy, a systemd unit, and create a birdy system user in the bird group:
# Debian / Ubuntu
sudo apt install ./birdy_*_amd64.deb
# RHEL / Fedora
sudo dnf install ./birdy-*.x86_64.rpmThe package does not start birdy — set it up first, as the post-install message explains:
sudo birdy init --db /var/lib/birdy/birdy.db --asn 64496 --router-id 192.0.2.1
sudo systemctl enable --now birdyIt recommends bird2 but does not force it. apt purge removes the database (which holds
BGP passwords); a plain apt remove keeps it. (The .apk is provided for convenience; Alpine
uses OpenRC, so you supply your own service under it.)
go install
Requires Go 1.25+. The binary is static (CGO_ENABLED=0); SQLite is
modernc.org/sqlite, so there is nothing to link against.
go install github.com/floreabogdan/birdy/cmd/birdy@latestOr cross-compile from anywhere and copy one file to the router:
COMMIT=$(git rev-parse HEAD)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \
-ldflags="-s -w -X github.com/floreabogdan/birdy/internal/buildinfo.Commit=$COMMIT" \
-o birdy ./cmd/birdy
scp birdy root@router:/usr/local/bin/birdyDocker
Run it on the same host as BIRD, sharing BIRD's control socket into the container. A prebuilt multi-arch image is published to the GitHub Container Registry:
# one-time: create the database and admin account
docker run --rm -it -v birdy-data:/var/lib/birdy \
ghcr.io/floreabogdan/birdy:latest \
init --asn 64496 --router-id 192.0.2.1 --label rtr1
# run the viewer, reachable only on the host's loopback
docker run -d --name birdy --restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-v birdy-data:/var/lib/birdy \
-v /run/bird:/run/bird:ro \
ghcr.io/floreabogdan/birdy:latestThe image bundles the bird binary so bird -p syntax checks work inside the container. See
docker-compose.yml for a Compose setup and the notes on enabling apply.
Then, on the router:
birdy doctor # preflight: can it reach BIRD? can it write what it needs?
birdy init --asn 64496 --router-id 192.0.2.1 --label rtr1
sudo systemctl enable --now birdy # the package installs the unit; no flags to addbirdy init prompts for an admin password. It reads BIRD's control socket (/run/bird/bird.ctl by
default), so it needs to run as a user in BIRD's group — the packaged unit
(deploy/birdy.service) runs it as an unprivileged birdy user in group
bird, with ProtectSystem=strict. Run init under sudo if you like: it hands the database it
creates to that account, so the service can write its own state.
birdy then listens on port 8080 on every interface, and enables whatever the router can do. Two things follow from that, and both are one setting away:
- Set the access list. Settings → Access control takes the IPs allowed to reach birdy at all;
anything else has its connection closed with no response. Until you do, birdy accepts connections
from anywhere and says so on the dashboard. There is no TLS by default — on a public address the
login crosses the network in the clear, so either terminate TLS in birdy itself
(
--tls-cert/--tls-key), restrict it to a management range you trust, or run it closed withbirdy server --listen 127.0.0.1:8080and an SSH tunnel. - Run it as a viewer, if you prefer. Add
--read-onlyto the unit and birdy never writesbird.confor issues a write command to BIRD. Out of the box it can write — but only when you press Adopt and then Apply, both deliberate acts with a diff, a backup and an armed auto-revert.
Observe
- Live dashboard of every BIRD protocol, split into BGP sessions and infrastructure
- Per-peer detail: BGP state, channels, import limits, and the raw control-socket output
- Route browser per session — imports, exports, and what was rejected on export
- On-demand looking glass (
show route for …) - Ping and traceroute from the router itself (on when those tools are installed) — a reachability looking glass to go alongside the route one
- Timeline of session transitions, flaps, and prefix-limit hits — interleaved with an audit trail of operator actions: who changed which peer or policy, and every config apply or revert
- Alerts to any number of destinations — Slack, Discord, email (SMTP), or a generic JSON webhook — when a session drops, recovers, flaps, hits its limit, or a config is applied/reverted; with per-destination event filtering and repeat-suppression
- An alert when BIRD itself becomes unreachable — the one failure session alerts can't catch
- A config-drift alert when
bird.confchanges outside birdy — a hand edit, abirdcreconfigure, or a revert birdy did not perform - Route-count history charts, on the dashboard grid and per peer, from samples birdy records itself — no Prometheus or Grafana needed to see when a session started leaking. Hover one and it names the point under the cursor: how many routes, and when
- A Prometheus
/metricsendpoint (on once the access list is narrowed) and a public/healthzprobe - Login rate-limiting (per-IP lockout) and a downloadable off-box backup bundle
- Live BIRD-code preview on every editor: the generated config updates as you type, before you save
- Every table paginated with numbered pages — the route browsers, the timeline, the apply history, and each library list
- Stable or development update tracking in the panel, with the installed build and available upstream release or commit shown without allowing the privileged web process to self-update
- Read-only multi-instance dashboard targets, selected from the top bar and authenticated with a dedicated dashboard token; configuration actions stay on the local router
- A collapsible, contextual navigation shell; keyboard command palette; saved dashboard filters and columns; and an explicit local/remote router indicator
- A per-user theme saved on your account, not the browser, so it follows you across machines: light / dark / system mode plus an accent colour — Green, Ocean, Violet, or Amber
Model
- Peers with roles (upstream, IX peer, customer, iBGP), which drive automatic origin tagging
- Guided peer profiles for transit, IX route-server, bilateral/PNI, customer, and iBGP sessions. Profiles fill conservative transport and limit defaults but never choose export policy, save, or apply.
- Disable a session from the peers list: it renders BIRD's
disabled, so BIRD stops connecting entirely — and birdy reads that back as disabled, not as a session that failed - iBGP with next-hop-self and route reflection; AS-path prepending, export communities, one-click 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 bgpwith each linked peer declaredfromit - 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) - Seed peers from the running BIRD — scaffold the model from the sessions BIRD already runs, so adopting a router is a review-and-import rather than re-typing every session by hand
- RFC 7999 customer blackhole (RTBH); PeeringDB lookups on the peer form
- BMP monitoring stations (RFC 7854) — stream every session's pre- and post-policy RIB to a collector
- Bogon prefixes and bogon ASNs, editable, in Settings
- RPKI: RTR servers and per-policy validation (log-only or drop-invalid). The dry run says how many routes BIRD is tagging invalid right now — the number you would drop by enforcing — and lists them; a table shows every import policy, whether it validates, and which peers ride on it
- A raw config block for everything birdy does not model, checked by
bird -pbefore it saves
Preview and apply
- The whole candidate
bird.conf, rendered from the model, with a syntax check viabird -p - A unified diff against the running config
- A four-stage readiness summary for rendering, syntax, policy review, and apply state
- A linter for what
bird -pcannot catch: route leaks, sessions that would accept nothing, unreachable filter branches, an RTR server nobody validates against - Apply (when not read-only): back up the current file, write the new one,
configure checkon the daemon, thenconfigure [soft] timeout— BIRD holds the new config with an armed auto-revert. Confirm within the window to keep it; do nothing and BIRD reverts on its own. Soft reload re-runs filters without bouncing sessions. - The authorship guard: birdy stores a hash of what it wrote and refuses to overwrite a
bird.confit did not author. A hand-managed file must be explicitly adopted (which backs it up). - Apply history: every applied config is kept — browse it, diff any version against what is
running, and re-apply an old one (the emergency-rollback path).
birdy doctorchecks readiness.
For apply to work, birdy needs write access to bird.conf and its directory, and --bird-conf must
be the same path BIRD was started with (bird -c). Passwords go to disk (BIRD needs them) but are
still masked everywhere in the browser.
birdy listens on every interface, and serves plaintext HTTP by default. It ships that way on purpose — a router UI that needs a config file edited before it answers is a UI nobody sets up — but it means the first thing to do after logging in is narrow who can reach it.
The IP allow-list (Settings → Access control) is that control: every request from an address you
did not list has its connection closed with no response at all. Loopback is always allowed, so an SSH
tunnel can never lock you out. While the list still allows everything, birdy says so once in its
startup log and flags it on that settings page. The unauthenticated /metrics endpoint is gated on it
too — no cookie can protect a Prometheus scrape, so
it stays closed until the list is narrowed, and starts serving the moment it is.
That is not encryption. On a public address the login and session cookie cross the network in the
clear, and an allow-list does nothing about interception — only about who may connect. Either
terminate TLS in birdy itself — pass a PEM certificate and key (--tls-cert/--tls-key) and it
serves native HTTPS (TLS 1.2+) — or, if the router is on the public internet, restrict it to a
management range you control, or run it closed:
birdy server --listen 127.0.0.1:8080 # then: ssh -L 8080:127.0.0.1:8080 routerAn audit log on the timeline records every operator action, attributed to the user who made it.
BGP MD5 session passwords are stored in the clear in birdy's SQLite database, because that is the
form BIRD needs them in. The database file is therefore as sensitive as bird.conf itself. Passwords
are never rendered into the browser: the peer form shows a blank field meaning "unchanged", and both
sides of the config diff are masked.
If you want a pure viewer, add --read-only to the unit: birdy then never writes bird.conf and never
issues a write command to BIRD.
go test ./...All addresses and AS numbers in the test fixtures — and in the screenshots above — are from the documentation ranges of RFC 5398, RFC 5737 and RFC 3849.
The UI is server-rendered html/template with go:embed and a little vanilla JavaScript. There is
no node build step and there will not be one. PLAN.md is the original design document —
the reasoning behind the data model and the milestone thinking — kept as a record; the product has
since moved past it, so read it as design intent, not current truth.
Built and maintained by Bogdan — AS210622.
- Aaran — AS204208 / AS47272 — kernel export hardening, import community tagging, native HTTPS, HTTP/session security, dashboard model coverage, multi-instance observation, runtime optimization, and the modern guided operator interface.
Contributions are welcome — open an issue or a pull request.
BSD Zero Clause — public-domain-equivalent. Do whatever you like with it; you owe no attribution and get no warranty.
The bundled webfonts are IBM Plex, copyright IBM Corp., used under the
SIL Open Font License 1.1 — see internal/web/static/fonts/LICENSE.txt.
That license covers the fonts only, not birdy.




