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.3.tar.gz (16.9 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.3-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.3.tar.gz
  • Upload date:
  • Size: 16.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.0.1 CPython/3.12.8

File hashes

Hashes for anemoi_dns-1.0.3.tar.gz
Algorithm Hash digest
SHA256 976d5933a0dd6e8c92283072d3432e1d08e266635bad8e4214a8964e0d13fcf7
MD5 7cf319362fcadf5a1593b30933a0ab35
BLAKE2b-256 151830d92cf0c99c54177d82cf2d57a14107d479701afcb2770097fa45355c36

See more details on using hashes here.

Provenance

The following attestation bundles were made for anemoi_dns-1.0.3.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.3-py3-none-any.whl.

File metadata

  • Download URL: anemoi_dns-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.0.1 CPython/3.12.8

File hashes

Hashes for anemoi_dns-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 cdb75713c54b48938df9b8256cb52555c405dc64af4264fd8f4c333f93a9f301
MD5 3fa4afaf1e6c18ea54760f85326355c7
BLAKE2b-256 cd45bea8883b17f1f2d34d3d4950ee9b7d3458388293c25a0967608e2542f9ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for anemoi_dns-1.0.3-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

1.0.4

2 files

This release

1.0.3 This release

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