Skip to main content

A least privilege dynamic DNS server

Project description

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.

Project details


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.5.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

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

Uploaded Python 3

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.5.tar.gz
  • Upload date:
  • Size: 17.1 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.5.tar.gz
Algorithm Hash digest
SHA256 feea885b2cb8d775052ac677cf49844912e1bc3d38cb4cac461223875dd8c105
MD5 9fe240db4f8a72920a558fb8557de870
BLAKE2b-256 6b9942a38431a34ca51d6eadc47bb2c1ec0665fc0c0f7acf1a168c59a0ee2bd2

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: anemoi_dns-1.0.5-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.5-py3-none-any.whl
Algorithm Hash digest
SHA256 b3e2bdd2e81275495fea9e8c11073919191548242c8712dd080d6d7c8967a063
MD5 59e891c8a55c6ae0403d2f0e5d744a32
BLAKE2b-256 681f266ede7f9ad29dd6c46fb1bf347d2f78636b490a2c42d3163fedb9135ab8

See more details on using hashes here.

Provenance

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page