Skip to main content

anemoi

Anemoi is a least privilege dynamic DNS server. See the blog post for more info.

Installation

For production systems, install with:

pip install anemoi-dns

For development purposes, clone and install locally:

git clone https://github.com/dayt0n/anemoi && cd anemoi
pip install -e .

Usage

Configuration

Domains and backends are specified with a YAML configuration file. An example config file is provided at example_config.yml.

Domains

You can have multiple domains on one Anemoi instance. To do this, create a config.yml file that looks something like this:

domains:
  - zone: random-domain.org
    provider: cloudflare
    token: AAAAAAAAAAAAAAAAAAAAAAAAAAA

  - zone: mydomain.com
    provider: cloudflare
    email: admin-user@yourdomain.com
    key: asfdasfdasddfasddfasdfasdf

  - zone: website.com
    provider: porkbun
    apikey: pk1_asdfasdfasdfasdfadsf
    secret: sk1_lkjhlkjhlkjhlkjhlkjh

The provider field can be any of:

  • cloudflare
    • takes: token OR email + key
  • porkbun
    • takes: apikey + secret

Backend

A backend must be specified in the config file like:

backend:
  type: database
  vendor: sqlite
  path: /home/me/my-sqlite.db

type can be one of:

  • tinydb
  • database

vendor is only necessary for database (for now) and can be one of:

  • sqlite
  • postgres

path is either a file path or full database connection URL.

Running the server in development

All commands require you to use a -c /path/to/config.yml unless you want to use the default config path.

anemoi -c /path/to/config.yml -v server

Running the server in production

You can use gunicorn to run the server after installing Anemoi:

gunicorn -b 0.0.0.0:80 'anemoi.server:setup_server("/path/to/config.yml")'

Creating a new client

To create a new client, run:

anemoi -c /path/to/config.yml client add -d yoursub.domain.com

This will give you a UUID and secret to use.

Deleting a client

If you believe a client has been compromised, you can revoke its access by deleting it.

To delete a client, run:

anemoi client delete -d yoursub.domain.com

Listing current clients

To see a list of current registered clients, run:

anemoi client list

Running a client

A client is just a fancy word for a single web request. The request must contain a JSON uuid and secret field, and that's it. It can be done using a curl command:

curl -X POST http://an.anemoi-server.com/check-in -H 'Content-Type: application/json' \
-d '{"uuid":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret":"averylongsecrethere"}'

If GET requests are more your speed, that also works:

curl 'http://an.anemoi-server.com/check-in?uuid=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&secret=averylongsecrethere'

This also means you can use any GET-based dynamic DNS client, such as the one in pfSense.

By default, Anemoi resolves the connecting client's IP on its own. However, if you want to manually pass an IP parameter, you can do that by appending an ip value with ?uuid=...&secret=...&ip=123.123.123.123 in a GET request or in a POST request body like {"uuid": "...", "secret": "...", "ip": "123.123.123.123"}.

Development

Before adding any pull requests, make sure you have pre-commit installed, then add the hooks for this repo:

pre-commit install

Anemoi allows you to have multiple DNS provider types as well as backend types to store your client data.

Providers

Adding a new DNS provider should be fairly simple.

Let's say there is a DNS provider, like Cloudflare, called Groundwater. To add Groundwater as a dynamic DNS provider, do the following:

  1. Create a file called anemoi/providers/groundwater.py.
  2. Add a class in that file called GroundwaterProvider(Provider). The class should have a skeleton like:
class GroundwaterProvider(Provider):
    key: str = ""
    def __init__(self, config):
      # parse config to get Groundwater API keys and such, return None on failure
      if key := config.get("key"):
          self.key = key
      else:
          return None

    # returns list of {'A': '1.1.1.1'} objects
    def get_record_ips(self, subdomain) -> List[Dict[str, str]]:
        # query API here, then return the records as a dictionary
        result = requests.get(f"https://groundwater.dev/api/get_records/{subdomain}").json()["records"]
        """
        imagine the result looks like:
        [
            {
                "domain":"test.groundwater-test.dev",
                "type": "A",
                "ip": "1.1.1.1",
                "ttl": 600,
            }
        ]
        """
        return [{x['type']: x['ip']} for x in records]

    # returns bool of if the update succeeded or not
    def update_record_ip(self, subdomain: str, ip, rtype="A") -> bool:
        if not is_ip_record_valid(ip, rtype):
            return False
        # parse out domain name, then update the IP with the record type rtype
        #   on the Groundwater API here
        records = requests.get(f"https://groundwater.dev/api/get_records/{subdomain}").json()["records"]
        if not records:
            # create new record
            result = requests.post(f"https://groundwater.dev/api/create_record/{subdomain}/{rtype}/{ip}").json()
            if result.get("status") == "success":
                return True
            return False
        # update existing record
        for record in records:
            if ip == record["ip"]:
                # don't update record if not necessary
                continue
            result = requests.post(f"https://groundwater.dev/api/update_record/{subdomain}/{rtype}/{ip}").json()
            if result.get("status") != "success":
                return False
        return True
  1. Use your provider in the config:
domains:
  - zone: groundwater-test.com
    key: asdfasdflkjhlkjh
    provider: groundwater

Backends

All data storage backends must inherit the Backend class. The skeleton of the backend should implement the following methods:

class YourBackend(Backend):

    def __init__(self, config: Dict):
        # do something with your {'type':'aaa', 'vendor': 'bbb', 'path': 'ccc'} config here
        pass

    def add_client(self, client: Client):
        pass

    # return UUID if success, None if fail
    def delete_client(self, client: Client) -> Optional[str]:
        return None

    # return Client() object if success, None if fail
    def get_client(
        self, uuid: Optional[str] = None, domain: Optional[str] = None
    ) -> Optional[Client]:
        return None

    def update_ip(self, client: Client, ip: str, version: int):
        pass

    @property
    def clients(self) -> List[Client]:
        return []

anemoi.backends.database and anemoi.backends.tinydb may be useful to look at as you are creating your new data storage backend.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anemoi_dns-1.0.4.tar.gz (17.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anemoi_dns-1.0.4-py3-none-any.whl (16.1 kB view details)

Uploaded Python 3

File details

Details for the file anemoi_dns-1.0.4.tar.gz.

File metadata

  • Download URL: anemoi_dns-1.0.4.tar.gz
  • Upload date:
  • Size: 17.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for anemoi_dns-1.0.4.tar.gz
Algorithm Hash digest
SHA256 e7f60773c07a8a1e430715362b9b0b6c739317f3c841b5ebe253582b5f9512c5
MD5 f74d153ca42b889c8166828c080e2890
BLAKE2b-256 d50a77bc101cd772bcb46ada998ac0e58ad6bbd6b31589731c1e0b9662df690c

See more details on using hashes here.

Provenance

The following attestation bundles were made for anemoi_dns-1.0.4.tar.gz:

Publisher: publish.yaml on dayt0n/anemoi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file anemoi_dns-1.0.4-py3-none-any.whl.

File metadata

  • Download URL: anemoi_dns-1.0.4-py3-none-any.whl
  • Upload date:
  • Size: 16.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for anemoi_dns-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 3d601f4eeb968fd5f3e60913e4e8e25d43e7d418d04b31fd50f6b2a286894d27
MD5 6b9170c07e9bd26e3887c576f38555e5
BLAKE2b-256 bff022fb3df7a0b6915630fcccb9a72ed840fa50533e7afdc707d4fc6422bab4

See more details on using hashes here.

Provenance

The following attestation bundles were made for anemoi_dns-1.0.4-py3-none-any.whl:

Publisher: publish.yaml on dayt0n/anemoi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.5

2 files

This release

1.0.4 This release

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page