271 lines
8.8 KiB
Markdown
271 lines
8.8 KiB
Markdown
# dynDNS
|
||
|
||
Lightweight dynDNS backend for BIND-based dynamic DNS updates.
|
||
|
||
This repository contains only the backend components of the dynDNS service: the main `dyndns.sh` script and a `vars.example` configuration template. It is designed as a backend component behind a web frontend or API that manages member accounts and IP assignments.
|
||
|
||
## Outline
|
||
|
||
- [Features](#features)
|
||
- [Installation](#installation)
|
||
- [MariaDB Initialization](#mariadb-initialization)
|
||
- [Database Schema](#database-schema)
|
||
- [Configuration](#configuration)
|
||
- [Directory Layout](#directory-layout)
|
||
- [CRON Job Example](#cron-job-example)
|
||
- [License](#license)
|
||
- [Authors](#authors)
|
||
- [Project Home](#project-home)
|
||
|
||
## Features
|
||
|
||
- Dynamic DNS backend script for BIND-based environments
|
||
- Database-driven record management
|
||
- A and AAAA record handling
|
||
- Wildcard CNAME generation for managed subdomains
|
||
- `nsupdate` integration with TSIG authentication
|
||
- DNSSEC-aware update workflow
|
||
- Simple configuration through a separate vars file
|
||
- Designed for self-hosted Linux environments
|
||
|
||
## Installation
|
||
|
||
1. Clone the repository.
|
||
2. Copy `vars.example` to a site-specific vars file.
|
||
3. Adjust paths, database settings, BIND zone settings and TSIG key references.
|
||
4. Make `dyndns.sh` executable.
|
||
5. Run the script manually once before automation.
|
||
|
||
Example:
|
||
|
||
```sh
|
||
sudo groupadd --system dyndns
|
||
sudo install -d -m 0750 -o root -g dyndns /opt/dyndns
|
||
sudo git clone https://dev.town-square.de/cb601/dyndns.git /opt/dyndns
|
||
|
||
cd /opt/dyndns
|
||
|
||
sudo cp vars.example vars
|
||
sudo chown root:dyndns /opt/dyndns/dyndns.sh /opt/dyndns/vars
|
||
sudo chmod 0750 /opt/dyndns/dyndns.sh
|
||
sudo chmod 0640 /opt/dyndns/vars
|
||
|
||
sudo /opt/dyndns/dyndns.sh help
|
||
```
|
||
|
||
The `dyndns.sh` script is owned by root and readable by the dyndns group only. This keeps the contents of the `vars` file (including database credentials) restricted to the backend.
|
||
|
||
## MariaDB Initialization
|
||
|
||
Before running the dynDNS backend for the first time, create the application database and a dedicated database user. Using a dedicated user with privileges limited to the dynDNS database is the recommended setup.
|
||
|
||
Connect to MariaDB as an administrative user:
|
||
|
||
```sh
|
||
mariadb -u root -p
|
||
```
|
||
|
||
Alternatively, when using the system root account and a configured `/root/.my.cnf`:
|
||
|
||
```sh
|
||
sudo mariadb
|
||
```
|
||
|
||
Create the database and grant access to a dedicated backend user:
|
||
|
||
```sql
|
||
CREATE DATABASE dyndns;
|
||
|
||
CREATE USER 'dyndnsuser'@'localhost' IDENTIFIED BY '<REPLACE_WITH_STRONG_PASSWORD>';
|
||
CREATE USER 'dyndnsuser'@'::1' IDENTIFIED BY '<REPLACE_WITH_STRONG_PASSWORD>';
|
||
CREATE USER 'dyndnsuser'@'127.0.0.1' IDENTIFIED BY '<REPLACE_WITH_STRONG_PASSWORD>';
|
||
|
||
GRANT ALL PRIVILEGES ON dyndns.* TO 'dyndnsuser'@'localhost';
|
||
GRANT ALL PRIVILEGES ON dyndns.* TO 'dyndnsuser'@'::1';
|
||
GRANT ALL PRIVILEGES ON dyndns.* TO 'dyndnsuser'@'127.0.0.1';
|
||
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
This setup allows local access for the dedicated dynDNS user from `localhost`, `127.0.0.1` and `::1` and keeps the dynDNS backend user restricted to the `dyndns` database and local connections only.
|
||
|
||
After database initialization, adjust the matching settings in your vars file:
|
||
|
||
```sh
|
||
DYNDNS_SQL_HOST="localhost"
|
||
DYNDNS_SQL_DATABASE="dyndns"
|
||
DYNDNS_SQL_USER="dyndnsuser"
|
||
DYNDNS_SQL_PASS="<REPLACE_WITH_STRONG_PASSWORD>"
|
||
```
|
||
|
||
Replace `<REPLACE_WITH_STRONG_PASSWORD>` with a site‑specific, strong password.
|
||
|
||
A first manual connection test is recommended before enabling cron-based automation, so database connectivity and privileges can be verified early.
|
||
|
||
## Database Schema
|
||
|
||
The dynDNS backend expects a MariaDB schema with three core tables:
|
||
|
||
- `members` for account and hostname ownership data
|
||
- `connections` for the currently assigned IP address per member
|
||
- `login_attempts` for tracking login attempts
|
||
|
||
The following definitions reflect the currently used database layout.
|
||
|
||
### Table: `members`
|
||
|
||
Stores dynDNS account identity and hostname ownership data.
|
||
|
||
```sql
|
||
CREATE TABLE `members` (
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
`username` varchar(63) NOT NULL,
|
||
`domain` varchar(253) NOT NULL,
|
||
`email` varchar(254) NOT NULL,
|
||
`password` varchar(255) NOT NULL,
|
||
`salt` varchar(255) NOT NULL,
|
||
`timestamp` bigint(20) unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `username` (`username`,`domain`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
```
|
||
|
||
Field overview:
|
||
|
||
- `id`: Internal primary key
|
||
- `username`: Host-specific account name, usually the subdomain part
|
||
- `domain`: Managed DNS zone name
|
||
- `email`: Contact address of the member
|
||
- `password`: Stored password hash
|
||
- `salt`: Per-user salt used for password hashing
|
||
- `timestamp`: Record timestamp stored as unsigned Unix time (seconds since epoch)
|
||
- `UNIQUE (username, domain)`: Ensures that a hostname can only exist once per zone
|
||
|
||
### Table: `connections`
|
||
|
||
Stores the currently active IP address assignment for a dynDNS member.
|
||
|
||
```sql
|
||
CREATE TABLE `connections` (
|
||
`id` int(11) NOT NULL,
|
||
`IP` varchar(45) NOT NULL,
|
||
`IPv6_flag` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||
`IP_locked` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||
`timestamp` bigint(20) unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
CONSTRAINT `fk_connections_members`
|
||
FOREIGN KEY (`id`) REFERENCES `members` (`id`)
|
||
ON DELETE CASCADE
|
||
ON UPDATE RESTRICT
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
```
|
||
|
||
Field overview:
|
||
|
||
- `id`: Shared primary key and foreign key referencing `members.id`
|
||
- `IP`: Current IP address in text form; `varchar(45)` supports both IPv4 and IPv6
|
||
- `IPv6_flag`: `0` for A records, `1` for AAAA records
|
||
- `IP_locked`: Prevents automated DNS updates when set
|
||
- `timestamp`: Record timestamp stored as unsigned Unix time (seconds since epoch)
|
||
|
||
### Table: `login_attempts`
|
||
|
||
Stores the login attempt counter for a member.
|
||
|
||
```sql
|
||
CREATE TABLE `login_attempts` (
|
||
`id` int(11) NOT NULL,
|
||
`count` smallint(5) unsigned NOT NULL DEFAULT 0,
|
||
`timestamp` bigint(20) unsigned NOT NULL,
|
||
PRIMARY KEY (`id`),
|
||
CONSTRAINT `fk_login_attempts_members`
|
||
FOREIGN KEY (`id`) REFERENCES `members` (`id`)
|
||
ON DELETE CASCADE
|
||
ON UPDATE RESTRICT
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
```
|
||
|
||
Field overview:
|
||
|
||
- `id`: Shared primary key and foreign key referencing `members.id`
|
||
- `count`: Number of tracked login attempts
|
||
- `timestamp`: Record timestamp stored as unsigned Unix time (seconds since epoch)
|
||
|
||
### Referential Integrity
|
||
|
||
The schema uses explicit foreign key constraints from `connections.id` and `login_attempts.id` to `members.id`. Both child tables use `ON DELETE CASCADE`, so dependent records are removed automatically when a member is deleted. This ensures database-level referential integrity and matches the intended one-to-one relationship design.
|
||
|
||
When deleting a member, the application only needs to remove the parent row from `members`; dependent rows in `connections` and `login_attempts` are removed automatically by the database.
|
||
|
||
### Notes
|
||
|
||
- The schema uses `utf8mb4` with `utf8mb4_unicode_ci`.
|
||
- `username` uses `varchar(63)`, which matches the maximum length of a single DNS label.
|
||
- `domain` uses `varchar(253)`, which matches the typical maximum length of a FQDN without trailing root dot.
|
||
- IP addresses are stored as text in `varchar(45)`, which is sufficient for IPv4 and IPv6 storage.
|
||
|
||
## Configuration
|
||
|
||
The backend is configured through a separate configuration file derived from `vars.example`.
|
||
|
||
Typical settings include:
|
||
|
||
- Base paths and log paths
|
||
- Database connection settings
|
||
- DNS zone name and zone file paths
|
||
- BIND server endpoint
|
||
- TSIG key file used for `nsupdate` and `rndc`
|
||
- Default TTL values
|
||
|
||
Recommended workflow:
|
||
|
||
1. Start from `vars.example`.
|
||
2. Create an environment-specific copy, for example `vars`.
|
||
3. Keep secrets and local overrides out of version control.
|
||
4. Validate database access and BIND update permissions before enabling automation.
|
||
|
||
## Directory Layout
|
||
|
||
```text
|
||
.
|
||
├── dyndns.sh
|
||
├── vars.example
|
||
├── README.md
|
||
├── Changelog.md
|
||
├── LICENSE
|
||
└── .gitignore
|
||
```
|
||
|
||
## CRON Job Example
|
||
|
||
The following example assumes the job is installed in the crontab of a dedicated service user or root, with sufficient read access to the vars file and execute permissions on `/opt/dyndns/dyndns.sh`.
|
||
|
||
```sh
|
||
*/5 * * * * /opt/dyndns/dyndns.sh update-zone >/dev/null 2>&1
|
||
```
|
||
|
||
Running the job every 5 minutes is a reasonable default for typical dynDNS use cases.
|
||
|
||
If a non-default vars file is used:
|
||
|
||
```sh
|
||
*/5 * * * * /opt/dyndns/dyndns.sh --vars /opt/dyndns/vars update-zone >/dev/null 2>&1
|
||
```
|
||
|
||
## License
|
||
|
||
[MIT](https://dev.town-square.de/cb601/dyndns/src/branch/main/LICENSE)
|
||
|
||
See `LICENSE` for details.
|
||
|
||
## Authors
|
||
|
||
CB-601 - the open tec Elevator
|
||
|
||
- [Stephan Düsterhaupt](xmpp:me@jabber.stephanduesterhaupt.de)
|
||
- [Ivo Noack](xmpp:me@jabber.ivonoack.de) aka Insonic
|
||
|
||
## Project Home
|
||
|
||
Project Home: [https://dev.town-square.de/cb601/dyndns](https://dev.town-square.de/cb601/dyndns)
|