Initial project commit

This commit is contained in:
2026-06-20 13:03:42 +02:00
parent 90326f5cb4
commit 3b82cf973f
6 changed files with 2087 additions and 15 deletions
+21
View File
@@ -0,0 +1,21 @@
# Ignore everything by default
*
# Allow directories
!*/
# Allow Git internals
!/.gitignore
# Allow shell scripts
!*.sh
# Allow Markdown files
!*.md
# Allow vars.example file
!vars.example
# Allow LICENSE and CHANGELOG
!LICENSE
!CHANGELOG.md
+30
View File
@@ -0,0 +1,30 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.0.0 - 2026-06-20
### Added
- Added a POSIX-compliant dynDNS management script for database-backed hostname lifecycle operations and DNS zone updates.
- Added the commands `add-hostname`, `check-hostname`, `remove-hostname`, and `update-zone`.
- Added hostname validation as well as subdomain and domain extraction helpers.
- Added database helper functions for SQL execution, scalar queries, and SQL string escaping.
- Added member provisioning in the database, including related records in `members`, `connections`, and `login_attempts`.
- Added salted SHA-512 password hashing for stored member credentials.
- Added runtime checks for required commands, directories, files, and environment variables.
- Added structured logging, temporary file handling, and cleanup helpers.
- Added dynamic DNS zone synchronization by comparing database records with the current zone file.
- Added DNS update generation and application through `nsupdate`, including `rndc` freeze and thaw handling.
### Changed
- Implemented hostname removal with automatic detection of foreign keys using `ON DELETE CASCADE`.
- Added a compatibility fallback for explicit child-table deletion when cascading foreign keys are not available.
### Fixed
- Fixed hostname removal compatibility across different database schema states by supporting both cascading and non-cascading delete paths.
+16 -13
View File
@@ -1,18 +1,21 @@
MIT License MIT License
Copyright (c) 2026 cb601 Copyright (c) 2026 CB-601 - the open tec Elevator
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy
associated documentation files (the "Software"), to deal in the Software without restriction, including of this software and associated documentation files (the "Software"), to deal
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell in the Software without restriction, including without limitation the rights
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
following conditions: copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial The above copyright notice and this permission notice shall be included in all
portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
USE OR OTHER DEALINGS IN THE SOFTWARE. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+269 -2
View File
@@ -1,3 +1,270 @@
# dyndns # dynDNS
dynDNS is a POSIX-compliant shell script for managing database-backed dynamic DNS hostnames and synchronizing them with a BIND zone. It handles hostname lifecycle operations (add, check, remove), maintains related database records and generates secure nsupdate transactions with optional DNSSEC and rndc integration. 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 sitespecific, 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](xmpp:me@jabber.stephanduesterhaupt.de))
- Ivo Noack aka Insonic ([XMPP](xmpp:me@jabber.ivonoack.de))
## Project Home
Project Home: [https://dev.town-square.de/cb601/dyndns](https://dev.town-square.de/cb601/dyndns)
+1614
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
# dynDNS configuration example
# Copy this file to 'vars' and adjust as needed.
# Warning: do not edit vars.example directly!
# ------------------------------------------------------------------
# GENERAL
# ------------------------------------------------------------------
# Base directory of dynDNS configuration (defaults to script directory)
#set_var DYNDNS "${0%/*}"
# Batch mode:
# Leave empty for interactive/normal console output.
# Set to any non-empty string to suppress notices on stdout.
#set_var DYNDNS_BATCH ""
# ------------------------------------------------------------------
# RUNTIME DIRECTORIES
# ------------------------------------------------------------------
# Temporary working directory used for per-run temp files and directories
#set_var DYNDNS_TEMP_DIR "$DYNDNS/tmp"
# ------------------------------------------------------------------
# LOGGING
# ------------------------------------------------------------------
# Directory for dynDNS log files
#set_var DYNDNS_LOG_DIR "$DYNDNS/log"
# Main log file
#set_var DYNDNS_LOG_FILE "$DYNDNS_LOG_DIR/dyndns_${DYNDNS_BIND_ZONE:-default}.log"
# File used to track the last zone update timestamp/state
#set_var DYNDNS_LOG_UPDATE_FILE "$DYNDNS_LOG_DIR/lastZoneUpdate_${DYNDNS_BIND_ZONE:-default}.log"
# Log level controls verbosity of logging output:
# 0 = off : Disable all logging output
# 1 = debug : Detailed diagnostics for troubleshooting and development
# 2 = info : Informational messages about normal operations
# 3 = warning : Warnings about unusual but non-fatal conditions
# 4 = error : Errors affecting functionality
# 5 = critical : Severe failures requiring immediate attention
#set_var DYNDNS_LOG_LEVEL 3
# ------------------------------------------------------------------
# DATABASE ACCESS
# ------------------------------------------------------------------
# Database client program.
# Example values:
# /usr/bin/mariadb
# mariadb
# /usr/bin/mysql
#set_var DYNDNS_DB_PROGRAM "/usr/bin/mariadb"
# Database host
#set_var DYNDNS_SQL_HOST "localhost"
# Optional client option group suffix.
# If set, dyndns uses:
# --defaults-group-suffix=$DYNDNS_SQL_GROUP
# and usually reads credentials from my.cnf / client config.
#set_var DYNDNS_SQL_GROUP "dyndns"
# Database name
#set_var DYNDNS_SQL_DATABASE "dyndns_database"
# Database login credentials.
# These are mainly used when DYNDNS_SQL_GROUP is unset/empty.
#set_var DYNDNS_SQL_USER "dyndns_user"
#set_var DYNDNS_SQL_PASS "strong_passphrase"
# Additional database client options, if needed.
# Example:
# "--protocol=tcp"
# "--socket=/run/mysqld/mysqld.sock"
#set_var DYNDNS_SQL_OPTIONS ""
# ------------------------------------------------------------------
# DNS / BIND
# ------------------------------------------------------------------
# Authoritative DNS server used for update operations
#set_var DYNDNS_BIND_SERVER "127.0.0.1"
# Service name used when reloading/reconfiguring the name server
#set_var DYNDNS_BIND_SERVICE "named.service"
# DNS zone handled by dynDNS
#set_var DYNDNS_BIND_ZONE "example24.com"
# TSIG key file or key identifier used for nsupdate access
#set_var DYNDNS_BIND_ZONE_KEY "keyfile"
# Directory containing the primary zone files
#set_var DYNDNS_BIND_ZONE_DIR "/var/named/zones"
# Main unsigned zone file
#set_var DYNDNS_BIND_ZONE_FILE "$DYNDNS_BIND_ZONE_DIR/$DYNDNS_BIND_ZONE.zone"
# Signed zone file, if DNSSEC signing is used externally
#set_var DYNDNS_BIND_ZONE_FILE_SIGNED "$DYNDNS_BIND_ZONE_DIR/$DYNDNS_BIND_ZONE.zone.signed"
# Default TTL for generated/managed records
#set_var DYNDNS_BIND_ZONE_TTL 300
# ------------------------------------------------------------------
# MEMBER DEFAULTS
# ------------------------------------------------------------------
# These values are typically supplied at runtime by command line options
# or frontend/backend integration, but they may also be preset here.
# Fully qualified hostname of the dynDNS member
#set_var DYNDNS_MEMBER_HOSTNAME "host.example24.com"
# Contact e-mail address of the dynDNS member
#set_var DYNDNS_MEMBER_EMAIL "admin@example24.com"
# Password or shared secret used during member creation/authentication
#set_var DYNDNS_MEMBER_PASS "strong_passphrase"
# ------------------------------------------------------------------
# OPTIONAL COMMAND HOOKS
# ------------------------------------------------------------------
# Reserved section for optional local extensions.
# Only enable such commands if your dyndns.sh implementation actually evaluates them.
# Commands to run before a zone update
# Example:
# set_var DYNDNS_CMD_PRE "/usr/local/libexec/dyndns-pre-update.sh"
# Commands to run after a successful zone update
# Example:
# set_var DYNDNS_CMD_POST "/usr/local/libexec/dyndns-post-update.sh"