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:9999/check-in -H 'Content-Type: application/json' \
-d '{"uuid":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "secret":"averylongsecrethere"}'

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

Uploaded Source

Built Distribution

anemoi_dns-1.0.1-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.1.tar.gz
  • Upload date:
  • Size: 16.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.6

File hashes

Hashes for anemoi_dns-1.0.1.tar.gz
Algorithm Hash digest
SHA256 9fa136f7b2b2b06ebe1faee3767ea02aea910f506eed54b3cd8b78fa9577ba27
MD5 ff3e684922caffe63a59cf41754be4fa
BLAKE2b-256 bc68d49d69fac25b128be9be3f1337ca5660f3a6625889f716f84108c51360d8

See more details on using hashes here.

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 15.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.6

File hashes

Hashes for anemoi_dns-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1325964bfd7a499a1c2d3f60bfe58a63963adc0317f9cbe6b3c3255759d61c86
MD5 d339664c1d61393bc651e7e3da815745
BLAKE2b-256 13a028c0ed59be12fff806681dc50f6851cc612fe5c5fd3f015bf9546384cef0

See more details on using hashes here.

Supported by

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