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

Uploaded Source

Built Distribution

anemoi_dns-1.0.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.0.tar.gz
  • Upload date:
  • Size: 16.5 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.0.tar.gz
Algorithm Hash digest
SHA256 44c7ac31fded46db2706f69836654ee8c9529d528f69eb01aeeeae3129f0dcd8
MD5 f94ccafdb72cd096c41058215c94452c
BLAKE2b-256 1ce8d07858cc441d0df1fa83643c3725d1bef4e1dafa3cd2112e35d061ef9157

See more details on using hashes here.

File details

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

File metadata

  • Download URL: anemoi_dns-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 16.6 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 212b232fc4d12b52a4c02dd3895bc940c5ae51484c3a82f57de3727f62ef6355
MD5 310795124196b9cb7943a8ab01958dea
BLAKE2b-256 6d056f88277d2fd1f3d79f1946f317f91433b230e628e36ea78132a3a3630000

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