{"id":38864814,"url":"https://github.com/gecio/anycastd","last_synced_at":"2026-01-17T14:24:38.008Z","repository":{"id":241140946,"uuid":"693202376","full_name":"gecio/anycastd","owner":"gecio","description":"Manage anycasted services based on health checks.","archived":false,"fork":false,"pushed_at":"2025-01-27T12:34:10.000Z","size":470,"stargazers_count":8,"open_issues_count":4,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-30T02:16:54.791Z","etag":null,"topics":["anycast","backend","bgp","daemon","healthcheck","high-availability","infrastructure","linux","load-balancing","monitoring","network","python","routing"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gecio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-18T14:51:19.000Z","updated_at":"2025-03-30T05:02:20.000Z","dependencies_parsed_at":"2024-05-22T16:57:46.115Z","dependency_job_id":"57cacdef-50c5-4541-ad20-21858c7ede90","html_url":"https://github.com/gecio/anycastd","commit_stats":null,"previous_names":["gecio/anycastd"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/gecio/anycastd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gecio%2Fanycastd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gecio%2Fanycastd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gecio%2Fanycastd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gecio%2Fanycastd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gecio","download_url":"https://codeload.github.com/gecio/anycastd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gecio%2Fanycastd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28509946,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T13:38:16.342Z","status":"ssl_error","status_checked_at":"2026-01-17T13:37:44.060Z","response_time":85,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anycast","backend","bgp","daemon","healthcheck","high-availability","infrastructure","linux","load-balancing","monitoring","network","python","routing"],"created_at":"2026-01-17T14:24:37.299Z","updated_at":"2026-01-17T14:24:38.000Z","avatar_url":"https://github.com/gecio.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\u003ccode\u003eanycastd\u003c/code\u003e\u003c/h1\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://github.com/gecio/anycastd/actions\"\u003e\n    \u003cimg src=\"https://github.com/gecio/anycastd/workflows/CI/badge.svg\" alt=\"CI status\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/gecio/anycastd\"\u003e\n    \u003cimg src=\"https://codecov.io/gh/gecio/anycastd/graph/badge.svg?token=DPOGLYZ26N)\" alt=\"code coverage\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/astral-sh/ruff\"\u003e\n    \u003cimg src=\"https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json\" alt=\"ruff\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/python/mypy\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Types-Mypy-blue.svg\" alt=\"typecheck\"\u003e\n  \u003c/a\u003e\n  \u003ca\u003e\n    \u003cimg src=\"https://img.shields.io/badge/v3.11+-black?style=flat\u0026color=FFFF00\u0026label=Python\" alt=\"python version\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/astral-sh/uv\"\u003e\n    \u003cimg src=\"https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json\" alt=\"uv\"\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\u003cbr\u003e\n\n`anycastd` functions as a daemon managing the announcement of network prefixes employed by redundant services using multiple backends that share a common set of service prefixes.\nEach prefix is announced individually to the network, forming a load-balancing strategy with redundancy, commonly referred to as Anycast.\nThis tool ensures that service prefixes are exclusively announced when all underlying service components are confirmed to be in a healthy state.\nBy doing so, `anycastd` prevents the attraction of traffic to service instances that may be malfunctioning, avoiding service diruption.\n\n## Table of Contents\n\n- [Usage Example](#usage-example)\n- [Services](#services)\n  - [Prefixes](#prefixes)\n    - [FRRouting](#frrouting)\n  - [Health Checks](#health-checks)\n    - [Cabourotte](#cabourotte)\n- [Configuration](#configuration)\n  - [Schema](#schema)\n\n## Usage Example\n\nIn the following example, we will use `anycastd` to manage the prefixes of two dual-stacked services commonly run on the same host. [FRRouting] is used to announce the prefixes of both services which are health checked through [Cabourotte].\n\n### `anycastd` configuration\n\nTo configure the two services in `anycastd`, we create the `/etc/anycastd/config.toml` configuration file with the following contents.\n\n```toml\n[services.dns]\nprefixes.frrouting = [\"2001:db8::b19:bad:53\", \"203.0.113.53\"]\nchecks.cabourotte = [\"dns\"]\n\n[services.ntp]\nprefixes.frrouting = [\n    { \"prefix\" = \"2001:db8::123:7e11:713e\", \"vrf\" = \"123\" },\n    { \"prefix\" = \"203.0.113.123\", \"vrf\" = \"123\" },\n]\nchecks.cabourotte = [\n    { \"name\" = \"ntp_v6\", \"interval\" = 1 },\n    { \"name\" = \"ntp_v4\", \"interval\" = 1 },\n]\n```\n\nThe first service, aptly named \"dns\", simply configures a DNS resolver service that announces the prefixes `2001:db8::b19:bad:53/128` \u0026 `203.0.113.53/32` through [FRRouting] as long as the [Cabourotte] health check `dns` is reported as healthy.\n\nThe second service, \"ntp\" is similar in functionality, although its configuration is a bit more verbose. Rather than omitting values that have a preconfigured default, a [VRF] as well as a health check interval are explicitly specified.\n\n### FRRouting configuration\n\nNext, we need to configure [FRRouting] so that `anycastd` can add and remove prefixes based on the services health checks. To do this, we create the `/etc/frr/frr.conf` with the following minimal configuration.\n\n```\n!\nrouter bgp 65536\n bgp router-id 203.0.113.179\n neighbor unnumbered peer-group\n neighbor unnumbered remote-as external\n neighbor unnumbered capability extended-nexthop\n neighbor eth0 interface peer-group unnumbered\n !\n address-family ipv4 unicast\n  redistribute static\n !\n address-family ipv6 unicast\n  redistribute static\n  neighbor fabric activate\n  neighbor fabric nexthop-local unchanged\n!\nrouter bgp 65537 vrf 123\n bgp router-id 203.0.113.181\n neighbor unnumbered peer-group\n neighbor unnumbered remote-as external\n neighbor unnumbered capability extended-nexthop\n neighbor eth1 interface peer-group unnumbered\n !\n address-family ipv4 unicast\n  redistribute static\n !\n address-family ipv6 unicast\n  redistribute static\n  neighbor fabric activate\n  neighbor fabric nexthop-local unchanged\n!\n```\n\nThis creates two BGP instances, `AS65536` in the default [VRF] and `AS65537` in [VRF] `123`.\nBoth of them have a single unnumbered session that will be used to advertise the service prefixes.\nThe most important statement here is `redistribute static` for both IPv4 and IPv6, instructing [FRRouting] to redistribute the static routes containing the service prefixes that will later be created by `anycastd`.\n\n### Cabourotte configuration\n\nThe last thing we have to configure is [Cabourotte], which performs the actual health checks. We create the following `/etc/cabourotte/config.yml`.\n\n```yaml\n---\nhttp:\n  host: 127.0.0.1\n  port: 9013\n\ndns-checks:\n  # Assumes that the DNS service is used as system wide resolver.\n  - name: dns\n    domain: check.local\n    timeout: 1s\n    interval: 5s\n    expected-ips: [\"2001:db8::15:600d\"]\n\ncommand-checks:\n  - name: ntp_v6\n    timeout: 3s\n    interval: 5s\n    command: ntpdate\n    arguments: [\"-q\", \"2001:db8::123:7e11:713e\"]\n  - name: ntp_v4\n    timeout: 3s\n    interval: 5s\n    command: ntpdate\n    arguments: [\"-q\", \"203.0.113.123\"]\n```\n\nThis sets up two fairly rudimentary health checks. The first renders healthy if a request to the DNS service for the `check.local` name returns the IPv6 address `2001:db8::15:600d` in the form of an `AAAA` record. The other two checks, `ntp_v6` and `ntp_v4` use the `ntpdate` CLI utility to determine if a date is returned by the NTP service.\n\n### Starting services\n\nTo finish up, we need to start our services. For this example we assume that both services as well as [Cabourotte] are run using [systemd] while `anycastd` is run directly for the purposes of this example.\n\nSo, to start the DNS, NTP and [Cabourotte] services we run\n\n```sh\n$ systemctl start dns.service ntp.service cabourotte.service\n```\n\nAfter which we can start `anycastd` itself.\n\n```sh\n$ anycastd run\n2024-03-25T15:17:23.783539Z [info     ] Reading configuration from /etc/anycastd/config.toml. config_path=/etc/anycastd/config.toml\n2024-03-25T15:17:23.785613Z [info     ] Starting service \"dns\".      service_health_checks=['dns'] service_healthy=False service_name=dns service_prefixes=['2001:db8::b19:bad:53', '203.0.113.53']\n2024-03-25T15:17:23.785613Z [info     ] Starting service \"ntp\".      service_health_checks=['ntp_v4', 'ntp_v6'] service_healthy=False service_name=ntp service_prefixes=['2001:db8::123:7e11:713e', '203.0.113.123']\n2024-03-25T15:17:23.797760Z [info     ] Service \"dns\" is now considered healthy, announcing related prefixes. service_health_checks=['dns'] service_healthy=True service_name=dns service_prefixes=['2001:db8::b19:bad:53', '203.0.113.53']\n2024-03-25T15:17:23.812260Z [info     ] Service \"ntp\" is now considered healthy, announcing related prefixes. service_health_checks=['ntp_v4', 'ntp_v6'] service_healthy=True service_name=ntp service_prefixes=['2001:db8::123:7e11:713e', '203.0.113.123']\n```\n\n`anycastd` will execute the health checks and, since all of them pass, announce the configured service IPs, which we can verify by looking at the new [FRRouting] running configuration.\n\n```diff\n@@ -7,9 +7,11 @@\n  neighbor eth0 interface peer-group unnumbered\n  !\n  address-family ipv4 unicast\n+  network 203.0.113.53/32\n   redistribute static\n  !\n  address-family ipv6 unicast\n+  network 2001:db8::b19:bad:53/128\n   redistribute static\n   neighbor fabric activate\n   neighbor fabric nexthop-local unchanged\n@@ -22,9 +24,11 @@\n  neighbor eth1 interface peer-group unnumbered\n  !\n  address-family ipv4 unicast\n+  network 203.0.113.123/32\n   redistribute static\n  !\n  address-family ipv6 unicast\n+  network 2001:db8::123:7e11:713e/128\n   redistribute static\n   neighbor fabric activate\n   neighbor fabric nexthop-local unchanged\n```\n\n### Stopping services\n\n`anycastd` will keep prefixes announced as long as health checks pass.\nTo stop announcing prefixes, even though the underlying services are healthy, for example to perform maintenance,\nsimply stop `anycastd`, causing all service prefixes to be denounced.\n\n```sh\n^C\n2024-03-25T15:20:29.738135Z [info     ] Received SIGINT, terminating.\n2024-03-25T15:20:29.817023Z [info     ] Service \"dns\" terminated.    service=dns\n2024-03-25T15:20:29.819003Z [info     ] Service \"ntp\" terminated.    service=ntp\n```\n\n## Services\n\nServices are the main unit of abstraction within `anycastd` and are used to form a logical relationship between health checks and network prefixes containing IP addresses related to the underlying application represented by the service. They work by continuously monitoring defined health checks and announcing/denouncing their prefixes based on\nthe combination of check results using the logic described below.\n\n```\n┌─[Service]─────────────┐                        ┌──────────┐\n│                       │                   ┌──\u003e │ HLTH CHK │\n│           ┌───────────────────────────────┤    └──────────┘\n│ IF healthy•:          │                   │    ┌──────────┐\n│     announce prefixes │                   ├──\u003e │ HLTH CHK │\n│ ELSE:           •─────────────────────┐   │    └──────────┘\n│     denounce prefixes │               │   │    ┌──────────┐\n└───────────────────────┘               │   └──\u003e │ HLTH CHK │\n                                        │        └──────────┘\n                                        │\n┌─[Routing Daemon]────────────────┐     │\n│ ┌──────────────────────────┐    │     │\n│ │ Prefix                   │ \u003c────────┤\n│ │ 2001:db8::b19:bad:53/128 │    │     │\n│ └──────────────────────────┘    │     │\n│ ┌──────────────────────────┐    │     │\n│ │ Prefix                   │ \u003c────────┘\n│ │ 203.0.113.53/32          │    │\n│ └──────────────────────────┘    │\n└─────────────────────────────────┘\n```\n\n### Prefixes\n\nRepresents a BGP network prefix that can be announced or denounced as part of the service.\nTypically, these are networks containing \"service IPs\", meaning the IP addresses exposed by a particular service, serving as the points of contact for clients to make requests while being completely agnostic to the specifics of anycast.\n\n**`anycastd` does not come with its own BGP implementation, but rather aims to provide abstractions\nthat interface with commonly used BGP daemons.** Supported BGP daemons along with their configuration options are described below.\n\n---\n\n#### FRRouting\n\nFree Range Routing, [FRRouting], or simply FRR is a free and open source Internet routing protocol suite for Linux and Unix platforms.\nAmongst others, it provides a BGP implementation that can be used to announce BGP service prefixes dynamically.\n\n##### Options\n\n| Option                     | Description                                                         | Default          | Examples                                                                 |\n| -------------------------- | ------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------ |\n| **prefix** \u003cbr\u003e (required) | The network prefix to create when healthy.                          | `null`           | `2001:db8:4:387b::/64` \u003cbr\u003e `192.0.2.240/28` \u003cbr\u003e `2001:db8::b19:bad:53` |\n| _vrf_                      | A VRF to create the prefix in. If omitted, the default VRF is used. | `None`           | `EDGE`                                                                   |\n| _vtysh_                    | The path to the vtysh binary used to configure FRRouting.           | `/usr/bin/vtysh` | `/usr/local/bin/vtysh`                                                   |\n\n##### Supported Versions\n\nWhile CI integration tests only target the latest version of FRRouting, we aim to support releases made within the last 6 months at minimum. `anycastd` is known to work with versions starting from `7.3.1`, although older versions are likely to work as well.\n\n### Health Checks\n\nAssessments on individual components constituting the service to ascertain the overall operational status of the service.\nA service is considered healthy as a whole if all of its health checks report a healthy status. Possible health check types along with their configuration options are described below.\n\n---\n\n#### Cabourotte\n\n[Cabourotte] is a general purpose healthchecking tool written in Golang that can be configured to execute checks, exposing their results via API.\n\n##### Options\n\n| Option                   | Description                                                           | Default                 | Examples                 |\n| ------------------------ | --------------------------------------------------------------------- | ----------------------- | ------------------------ |\n| **name** \u003cbr\u003e (required) | The name of the health check, as defined in [Cabourotte].             | `null`                  | `anycast-dns`            |\n| _url_                    | The base URL of the Cabourotte API.                                   | `http://127.0.0.1:9013` | `https:://healthz.local` |\n| _interval_               | The interval in seconds at which the health check should be executed. | `5`                     | `2`                      |\n\n---\n\n## Configuration\n\n`anycastd` can be configured using a TOML configuration file located at `/etc/anycastd/config.toml`, or a path specified through the `--configuration` parameter.\nFor a quick primer on TOML, see [A Quick Tour of TOML](https://toml.io).\n\n### Schema\n\n```toml\n[services] # A definition of services to be managed by `anycastd`.\n\n  [services.\u003cservice-name\u003e] # A service with a unique and recognizable name.\n    [[prefixes.\u003cprefix-type\u003e]] # A prefix of the specified type.\n      # Options related to the specified prefix type.\n\n    [[checks.\u003ccheck-type\u003e]] # A check of the specified type.\n      # Options related to the specified check type.\n```\n\n## Contributing\n\nContributions of all sizes that improve `anycastd` in any way, be it DX/UX, documentation, performance or other are highly appreciated.\nTo get started, please read the [contribution guidelines](.github/CONTRIBUTING.md). Before starting work on a new feature you would like to contribute that may impact simplicity, reliability or performance, please open an issue first.\n\n[Anycast]: https://en.wikipedia.org/wiki/Anycast\n[FRRouting]: https://github.com/FRRouting/frr\n[Cabourotte]: https://github.com/appclacks/cabourotte\n[VRF]: https://en.wikipedia.org/wiki/Virtual_routing_and_forwarding\n[systemd]: https://systemd.io/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgecio%2Fanycastd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgecio%2Fanycastd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgecio%2Fanycastd/lists"}