A lightweight anycast health checker that manages OSPF/BGP IP advertisement by adding and removing IPs on the loopback interface based on service health.
Designed to work with BIRD or any routing daemon that advertises IPs present on lo.
This solves a problem in a small package for my personal homelab.
- BIRD is configured to advertise any IP within a given prefix (e.g.
10.0.0.0/24) found on the loopback interface. - Anycaster runs health checks against local services on a configurable interval.
- When a service becomes healthy (after
riseconsecutive successes), anycaster adds its IP tolo— triggering OSPF/BGP advertisement. - When a service becomes unhealthy (after
fallconsecutive failures), anycaster removes the IP — withdrawing the route.
Only state transitions trigger IP changes. On startup, anycaster checks the current lo state for clean resume after restart.
Download a prebuilt binary from the releases page, or build from source:
cargo build --releaseThe binary is statically linked with rustls (no OpenSSL dependency).
anycaster --config /etc/anycaster/config.tomlLogging is controlled via the RUST_LOG environment variable:
RUST_LOG=debug anycaster -c config.tomlSee config.example.toml for a complete example.
[defaults]
check_interval = 5 # seconds between checks
rise = 3 # consecutive successes before advertising
fall = 2 # consecutive failures before withdrawing
on_exit = "withdraw" # "withdraw" or "maintain"Each [[service]] defines a service to monitor. Per-service check_interval, rise, and fall override the defaults.
Connect to a host:port. Healthy if the connection is established within the timeout.
[[service]]
name = "cache"
ip = "10.0.0.3"
type = "tcp"
host = "127.0.0.1"
port = 6379
timeout = 3 # seconds (default: 3)Send a GET request and check the response status code.
[[service]]
name = "web"
ip = "10.0.0.2"
type = "http"
url = "http://127.0.0.1:8080/healthz"
expect_status = 200 # default: 200
timeout = 5 # seconds (default: 3)Send DNS queries via hickory-dns (pure Rust, async, UDP). All queries must return NOERROR. If no queries are specified, a default A query for google.com is used.
[[service]]
name = "dns"
ip = "10.0.0.1"
type = "dns"
host = "127.0.0.1"
port = 53 # default: 53
timeout = 3 # seconds (default: 3)
queries = [
{ name = "google.com", record_type = "A" },
{ name = "example.com", record_type = "AAAA" },
]Supported record types: A, AAAA, MX, TXT, CNAME, NS, SOA, SRV, PTR.
Send an NTPv4 query and verify the server's stratum is within the acceptable range.
[[service]]
name = "ntp"
ip = "10.0.0.5"
type = "ntp"
host = "127.0.0.1"
port = 123 # default: 123
max_stratum = 1 # default: 1
timeout = 3 # seconds (default: 3)Stratum 0 (kiss-o'-death) always fails. Set max_stratum = 2 to accept stratum 1 and 2 servers.
Run an arbitrary command. Healthy if the exit code is 0.
[[service]]
name = "custom"
ip = "10.0.0.4"
type = "exec"
command = "/usr/local/bin/check_myservice.sh"
timeout = 3 # seconds (default: 3)On shutdown (SIGINT/Ctrl-C), anycaster either withdraws all managed IPs or leaves them in place, controlled by the on_exit setting:
withdraw(default) — remove all IPs fromlo, withdrawing BGP routesmaintain— leave IPs in place, keeping routes advertised until the next health check cycle (e.g. via a process supervisor restart)