# 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"
}