anemoi-dns


Nameanemoi-dns JSON
Version 1.0.3 PyPI version JSON
download
home_pageNone
SummaryA least privilege dynamic DNS server
upload_time2024-12-12 15:12:32
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseNone
keywords dynamic dns least privilege zero trust
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # anemoi

Anemoi is a least privilege dynamic DNS server. See [the blog post](https://dayt0n.com/articles/anemoi) for more info.

### Installation
For production systems, install with:
```bash
pip install anemoi-dns
```

For development purposes, clone and install locally:
```bash
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](https://github.com/dayt0n/anemoi/tree/main/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:
```yaml
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:
```yaml
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.

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

### Running the server in production
You can use gunicorn to run the server after installing Anemoi:
```bash
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:
```bash
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:
```bash
anemoi client delete -d yoursub.domain.com
```

### Listing current clients
To see a list of current registered clients, run:
```bash
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:
```bash
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:
```bash
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](https://docs.netgate.com/pfsense/en/latest/services/dyndns/client.html).

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`](https://pre-commit.com/) installed, then add the hooks for this repo:
```bash
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`](https://github.com/dayt0n/anemoi/tree/main/anemoi/providers/groundwater.py).
2. Add a class in that file called `GroundwaterProvider(Provider)`. The class should have a skeleton like:
```python
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
```
3. Use your provider in the config:
```yaml
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:
```python
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`](https://github.com/dayt0n/anemoi/tree/main/anemoi/backends/database.py) and [`anemoi.backends.tinydb`](https://github.com/dayt0n/anemoi/tree/main/anemoi/backends/tinydb.py) may be useful to look at as you are creating your new data storage backend.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "anemoi-dns",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "dynamic, dns, least, privilege, zero trust",
    "author": null,
    "author_email": "\"Dayton Hasty (dayt0n)\" <dayt0n@dayt0n.com>",
    "download_url": "https://files.pythonhosted.org/packages/15/18/30d92cf0c99c54177d82cf2d57a14107d479701afcb2770097fa45355c36/anemoi_dns-1.0.3.tar.gz",
    "platform": null,
    "description": "# anemoi\n\nAnemoi is a least privilege dynamic DNS server. See [the blog post](https://dayt0n.com/articles/anemoi) for more info.\n\n### Installation\nFor production systems, install with:\n```bash\npip install anemoi-dns\n```\n\nFor development purposes, clone and install locally:\n```bash\ngit clone https://github.com/dayt0n/anemoi && cd anemoi\npip install -e .\n```\n\n## Usage\n\n### Configuration\nDomains and backends are specified with a YAML configuration file. An example config file is provided at [example_config.yml](https://github.com/dayt0n/anemoi/tree/main/example_config.yml).\n\n#### Domains\nYou can have multiple domains on one Anemoi instance. To do this, create a `config.yml` file that looks something like this:\n```yaml\ndomains:\n  - zone: random-domain.org\n    provider: cloudflare\n    token: AAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n  - zone: mydomain.com\n    provider: cloudflare\n    email: admin-user@yourdomain.com\n    key: asfdasfdasddfasddfasdfasdf\n\n  - zone: website.com\n    provider: porkbun\n    apikey: pk1_asdfasdfasdfasdfadsf\n    secret: sk1_lkjhlkjhlkjhlkjhlkjh\n```\n\nThe `provider` field can be any of:\n- `cloudflare`\n  - takes: `token` OR `email` + `key`\n- `porkbun`\n  - takes: `apikey` + `secret`\n\n#### Backend\nA backend must be specified in the config file like:\n```yaml\nbackend:\n  type: database\n  vendor: sqlite\n  path: /home/me/my-sqlite.db\n```\n\n`type` can be one of:\n- `tinydb`\n- `database`\n\n`vendor` is only necessary for `database` (for now) and can be one of:\n- `sqlite`\n- `postgres`\n\n`path` is either a file path or full database connection URL.\n\n### Running the server in development\nAll commands require you to use a `-c /path/to/config.yml` unless you want to use the default config path.\n\n```bash\nanemoi -c /path/to/config.yml -v server\n```\n\n### Running the server in production\nYou can use gunicorn to run the server after installing Anemoi:\n```bash\ngunicorn -b 0.0.0.0:80 'anemoi.server:setup_server(\"/path/to/config.yml\")'\n```\n\n### Creating a new client\nTo create a new client, run:\n```bash\nanemoi -c /path/to/config.yml client add -d yoursub.domain.com\n```\n\nThis will give you a UUID and secret to use.\n\n### Deleting a client\nIf you believe a client has been compromised, you can revoke its access by deleting it.\n\nTo delete a client, run:\n```bash\nanemoi client delete -d yoursub.domain.com\n```\n\n### Listing current clients\nTo see a list of current registered clients, run:\n```bash\nanemoi client list\n```\n\n### Running a client\n\nA 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:\n```bash\ncurl -X POST http://an.anemoi-server.com/check-in -H 'Content-Type: application/json' \\\n-d '{\"uuid\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\", \"secret\":\"averylongsecrethere\"}'\n```\n\nIf `GET` requests are more your speed, that also works:\n```bash\ncurl 'http://an.anemoi-server.com/check-in?uuid=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&secret=averylongsecrethere'\n```\n\nThis also means you can use any GET-based dynamic DNS client, such as the one in [pfSense](https://docs.netgate.com/pfsense/en/latest/services/dyndns/client.html).\n\nBy 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\"}`.\n\n## Development\nBefore adding any pull requests, make sure you have [`pre-commit`](https://pre-commit.com/) installed, then add the hooks for this repo:\n```bash\npre-commit install\n```\n\nAnemoi allows you to have multiple DNS provider types as well as backend types to store your client data.\n\n### Providers\nAdding a new DNS provider should be fairly simple.\n\nLet's say there is a DNS provider, like Cloudflare, called `Groundwater`. To add Groundwater as a dynamic DNS provider, do the following:\n1. Create a file called [`anemoi/providers/groundwater.py`](https://github.com/dayt0n/anemoi/tree/main/anemoi/providers/groundwater.py).\n2. Add a class in that file called `GroundwaterProvider(Provider)`. The class should have a skeleton like:\n```python\nclass GroundwaterProvider(Provider):\n    key: str = \"\"\n    def __init__(self, config):\n      # parse config to get Groundwater API keys and such, return None on failure\n      if key := config.get(\"key\"):\n          self.key = key\n      else:\n          return None\n\n    # returns list of {'A': '1.1.1.1'} objects\n    def get_record_ips(self, subdomain) -> List[Dict[str, str]]:\n        # query API here, then return the records as a dictionary\n        result = requests.get(f\"https://groundwater.dev/api/get_records/{subdomain}\").json()[\"records\"]\n        \"\"\"\n        imagine the result looks like:\n        [\n            {\n                \"domain\":\"test.groundwater-test.dev\",\n                \"type\": \"A\",\n                \"ip\": \"1.1.1.1\",\n                \"ttl\": 600,\n            }\n        ]\n        \"\"\"\n        return [{x['type']: x['ip']} for x in records]\n\n    # returns bool of if the update succeeded or not\n    def update_record_ip(self, subdomain: str, ip, rtype=\"A\") -> bool:\n        if not is_ip_record_valid(ip, rtype):\n            return False\n        # parse out domain name, then update the IP with the record type rtype\n        #   on the Groundwater API here\n        records = requests.get(f\"https://groundwater.dev/api/get_records/{subdomain}\").json()[\"records\"]\n        if not records:\n            # create new record\n            result = requests.post(f\"https://groundwater.dev/api/create_record/{subdomain}/{rtype}/{ip}\").json()\n            if result.get(\"status\") == \"success\":\n                return True\n            return False\n        # update existing record\n        for record in records:\n            if ip == record[\"ip\"]:\n                # don't update record if not necessary\n                continue\n            result = requests.post(f\"https://groundwater.dev/api/update_record/{subdomain}/{rtype}/{ip}\").json()\n            if result.get(\"status\") != \"success\":\n                return False\n        return True\n```\n3. Use your provider in the config:\n```yaml\ndomains:\n  - zone: groundwater-test.com\n    key: asdfasdflkjhlkjh\n    provider: groundwater\n```\n\n### Backends\n\nAll data storage backends must inherit the `Backend` class. The skeleton of the backend should implement the following methods:\n```python\nclass YourBackend(Backend):\n\n    def __init__(self, config: Dict):\n        # do something with your {'type':'aaa', 'vendor': 'bbb', 'path': 'ccc'} config here\n        pass\n\n    def add_client(self, client: Client):\n        pass\n\n    # return UUID if success, None if fail\n    def delete_client(self, client: Client) -> Optional[str]:\n        return None\n\n    # return Client() object if success, None if fail\n    def get_client(\n        self, uuid: Optional[str] = None, domain: Optional[str] = None\n    ) -> Optional[Client]:\n        return None\n\n    def update_ip(self, client: Client, ip: str, version: int):\n        pass\n\n    @property\n    def clients(self) -> List[Client]:\n        return []\n```\n\n[`anemoi.backends.database`](https://github.com/dayt0n/anemoi/tree/main/anemoi/backends/database.py) and [`anemoi.backends.tinydb`](https://github.com/dayt0n/anemoi/tree/main/anemoi/backends/tinydb.py) may be useful to look at as you are creating your new data storage backend.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A least privilege dynamic DNS server",
    "version": "1.0.3",
    "project_urls": {
        "Homepage": "https://dayt0n.com/articles/anemoi/",
        "Issues": "https://github.com/dayt0n/anemoi/issues",
        "Repository": "https://github.com/dayt0n/anemoi"
    },
    "split_keywords": [
        "dynamic",
        " dns",
        " least",
        " privilege",
        " zero trust"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "cd45bea8883b17f1f2d34d3d4950ee9b7d3458388293c25a0967608e2542f9ac",
                "md5": "3fa4afaf1e6c18ea54760f85326355c7",
                "sha256": "cdb75713c54b48938df9b8256cb52555c405dc64af4264fd8f4c333f93a9f301"
            },
            "downloads": -1,
            "filename": "anemoi_dns-1.0.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "3fa4afaf1e6c18ea54760f85326355c7",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 16041,
            "upload_time": "2024-12-12T15:12:31",
            "upload_time_iso_8601": "2024-12-12T15:12:31.282816Z",
            "url": "https://files.pythonhosted.org/packages/cd/45/bea8883b17f1f2d34d3d4950ee9b7d3458388293c25a0967608e2542f9ac/anemoi_dns-1.0.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "151830d92cf0c99c54177d82cf2d57a14107d479701afcb2770097fa45355c36",
                "md5": "7cf319362fcadf5a1593b30933a0ab35",
                "sha256": "976d5933a0dd6e8c92283072d3432e1d08e266635bad8e4214a8964e0d13fcf7"
            },
            "downloads": -1,
            "filename": "anemoi_dns-1.0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "7cf319362fcadf5a1593b30933a0ab35",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 16907,
            "upload_time": "2024-12-12T15:12:32",
            "upload_time_iso_8601": "2024-12-12T15:12:32.604705Z",
            "url": "https://files.pythonhosted.org/packages/15/18/30d92cf0c99c54177d82cf2d57a14107d479701afcb2770097fa45355c36/anemoi_dns-1.0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-12 15:12:32",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "dayt0n",
    "github_project": "anemoi",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "anemoi-dns"
}
        
Elapsed time: 7.96153s