369 lines
11 KiB
Markdown
369 lines
11 KiB
Markdown
# DDNS Service
|
|
|
|
Dynamic DNS update service with CLI administration. Accepts HTTP(S) requests to update DNS A/AAAA records using RFC 2136 dynamic updates.
|
|
|
|
## Features
|
|
|
|
- HTTP(S) server for DynDNS-compatible updates
|
|
- Multiple endpoints with configurable parameter aliases
|
|
- Dual-stack IPv4/IPv6 support
|
|
- SQLite or MariaDB database backend
|
|
- Argon2 password hashing
|
|
- Rate limiting (separate limits for good/bad requests)
|
|
- TTL-based automatic record expiration
|
|
- Email notifications on expiry
|
|
- Syslog, file and stdout logging support
|
|
- Plain text / JSON responses according to Accept-Header (application/json)
|
|
- IDN/punycode hostname support
|
|
- Proxy support
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pip install git+https://git.ccc-rheintal.ch/spacefreak/ddns-service.git
|
|
|
|
# With MariaDB support:
|
|
pip install "ddns-service[mysql] @ git+https://git.ccc-rheintal.ch/spacefreak/ddns-service.git"
|
|
```
|
|
|
|
Requires Python 3.11+. Dependencies installed automatically: argon2-cffi, dnspython, jinja2, peewee (+ pymysql for mysql extra).
|
|
|
|
## Service setup
|
|
|
|
1. Create system user and group:
|
|
```bash
|
|
useradd -r -s /sbin/nologin ddns
|
|
```
|
|
|
|
2. Create directories:
|
|
```bash
|
|
mkdir -p /etc/ddns-service /var/lib/ddns-service /var/log/ddns-service
|
|
chmod 750 /etc/ddns-service /var/lib/ddns-service
|
|
chmod 770 /var/log/ddns-service
|
|
chown ddns:ddns /etc/ddns-service /var/lib/ddns-service /var/log/ddns-service
|
|
```
|
|
|
|
3. Config file:
|
|
```bash
|
|
wget -O /etc/ddns-service/config.toml https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/config.example.toml
|
|
|
|
# The config file must not be world readable!
|
|
chmod 640 /etc/ddns-service/config.toml
|
|
chown ddns:ddns /etc/ddns-service/config.toml
|
|
```
|
|
|
|
4. Service autostart
|
|
```bash
|
|
# Systemd setup
|
|
wget -O /etc/systemd/systemd/ddns-service.service https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/ddns-service.service
|
|
systemctl daemon-reload
|
|
systemctl enable ddns-service
|
|
|
|
# OpenRC setup
|
|
wget -O /etc/init.d/ddns-service https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/ddns-service-openrc.initd
|
|
wget -O /etc/conf.d/ddns-service https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/ddns-service-openrc.confd
|
|
rc-update add ddns-service default
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Example:
|
|
```toml
|
|
[daemon]
|
|
# host = "localhost" # default: "localhost" (use reverse proxy for public access)
|
|
# port = 8443 # default: 8443
|
|
# log_level = "INFO" # default: "INFO"
|
|
# log_target = "stdout" # default: "stdout", or "syslog", "file"
|
|
# syslog_socket = "/dev/log" # default: "/dev/log"
|
|
# syslog_facility = "daemon" # default: "daemon"
|
|
# log_file = "/var/log/ddns-service/ddns-service.log" # default, used if log_target = "file"
|
|
# log_file_size = 52428800 # default: 52428800 (50 MB in bytes)
|
|
# log_versions = 5 # default: 5 backup files
|
|
# log_requests = false # default: false
|
|
# ssl = false # default: false
|
|
ssl_cert_file = "/etc/ddns-service/cert.pem" # required if ssl = true
|
|
ssl_key_file = "/etc/ddns-service/key.pem" # required if ssl = true
|
|
# proxy_header = "" # default: "" (disabled), e.g. "X-Forwarded-For"
|
|
# trusted_proxies = [] # default: [], e.g. ["127.0.0.1", "10.0.0.0/8"]
|
|
|
|
[database]
|
|
# backend = "sqlite" # default: "sqlite", or "mariadb"
|
|
path = "/var/lib/ddns-service/ddns.db" # required for sqlite
|
|
|
|
[dns_service]
|
|
# dns_server = "127.0.0.1" # default: "127.0.0.1" (must be IP address)
|
|
# dns_port = 53 # default: 53
|
|
# dns_timeout = 5 # default: 5 (seconds)
|
|
# ddns_default_key_file = "/etc/ddns-service/ddns.key" # optional, BIND TSIG key file
|
|
# cleanup_interval = 60 # default: 60 (seconds, expired records cleanup)
|
|
|
|
# Per-zone TSIG key overrides (optional)
|
|
# [dns_service.zone_keys]
|
|
# "dyn.example.com" = "/etc/ddns-service/dyn-example.key"
|
|
|
|
[defaults]
|
|
# dns_ttl = 60 # default: 60
|
|
# expiry_ttl = 3600 # default: 3600
|
|
|
|
[email]
|
|
# enabled = false # default: false
|
|
smtp_host = "localhost" # required if email.enabled
|
|
# smtp_port = 25 # default: 25
|
|
# smtp_starttls = false # default: false
|
|
from_address = "ddns@example.com" # required if email.enabled
|
|
# change_notification_template = "/etc/ddns-service/change_notification.j2" # optional
|
|
# expiry_notification_template = "/etc/ddns-service/expiry_notification.j2" # optional
|
|
|
|
[rate_limit]
|
|
# enabled = true # default: true
|
|
# good_window_seconds = 60 # default: 60
|
|
# good_max_requests = 5 # default: 5
|
|
# bad_window_seconds = 60 # default: 60
|
|
# bad_max_requests = 3 # default: 3
|
|
# cleanup_interval = 60 # default: 60 (seconds, rate limiter cleanup)
|
|
```
|
|
|
|
### TSIG Authentication
|
|
|
|
For secure DNS updates, configure TSIG authentication:
|
|
|
|
1. Generate key on BIND server:
|
|
```bash
|
|
tsig-keygen -a hmac-sha256 ddns-key > /etc/bind/ddns.key
|
|
```
|
|
|
|
2. Include in `named.conf` and configure zone:
|
|
```
|
|
include "/etc/bind/ddns.key";
|
|
|
|
zone "dyn.example.com" {
|
|
type master;
|
|
file "/var/lib/bind/dyn.example.com.zone";
|
|
update-policy {
|
|
grant ddns-key zonesub ANY;
|
|
};
|
|
};
|
|
```
|
|
|
|
3. Copy key file to ddns-service host and configure:
|
|
```toml
|
|
[dns_service]
|
|
ddns_default_key_file = "/etc/ddns-service/ddns.key"
|
|
```
|
|
|
|
Key file format (generated by `tsig-keygen`):
|
|
```
|
|
key "ddns-key" {
|
|
algorithm hmac-sha256;
|
|
secret "base64-encoded-secret";
|
|
};
|
|
```
|
|
|
|
Without TSIG authentication, the DNS server must allow updates based on IP address (via `allow-update` directive).
|
|
|
|
### Endpoints
|
|
|
|
Configure one or more HTTP endpoints. If no endpoints are defined, a default endpoint at `/update` is created with standard parameter names.
|
|
|
|
```toml
|
|
[[endpoints]]
|
|
path = "/update"
|
|
[endpoints.params]
|
|
hostname = ["hostname", "host"]
|
|
ipv4 = ["myip", "ipv4", "ip4"]
|
|
ipv6 = ["myip6", "ipv6", "ip6"]
|
|
username = ["username", "user"]
|
|
password = ["password", "pass", "token"]
|
|
|
|
[[endpoints]]
|
|
path = "/nic/update"
|
|
[endpoints.params]
|
|
hostname = ["hostname"]
|
|
ipv4 = ["myip"]
|
|
ipv6 = ["myip6"]
|
|
username = ["username"]
|
|
password = ["password"]
|
|
```
|
|
|
|
**Default accepted parameter names** (first match wins):
|
|
| Value | Accepted Names |
|
|
|-------|----------------|
|
|
| hostname (FQDN) | hostname, host |
|
|
| ipv4 (IPv4 address) | myip, ipv4, ip4 |
|
|
| ipv6 (IPv6 address) | myip6, ipv6, ip6 |
|
|
| username | username, user |
|
|
| password | password, pass, token |
|
|
|
|
## CLI Usage
|
|
|
|
### Initialize Database
|
|
|
|
```bash
|
|
ddns-service --init-db
|
|
```
|
|
|
|
The database is automatically created and migrated when the daemon starts or any CLI command is executed.
|
|
|
|
**SQLite permissions:** The first CLI invocation creates the database file. Run it as the service user (`sudo -u ddns ddns-service --init-db`) so the daemon can write to it. Root can use CLI afterwards without issues.
|
|
|
|
To allow other users CLI access (SQLite only):
|
|
```bash
|
|
usermod -aG ddns myuser # add user to ddns group
|
|
chmod g+w /var/lib/ddns-service/ddns.db # enable group write
|
|
```
|
|
|
|
### User Management
|
|
|
|
```bash
|
|
# List users
|
|
ddns-service user list
|
|
|
|
# Add user (prompts for password)
|
|
ddns-service user add myuser user@example.com
|
|
|
|
# Delete user (fails if hostnames exist)
|
|
ddns-service user delete myuser
|
|
|
|
# Change password
|
|
ddns-service user passwd myuser
|
|
|
|
# Update email
|
|
ddns-service user email myuser new@example.com
|
|
```
|
|
|
|
### Hostname Management
|
|
|
|
```bash
|
|
# List all hostnames
|
|
ddns-service hostname list
|
|
|
|
# List hostnames for specific user
|
|
ddns-service hostname list --user myuser
|
|
|
|
# Add hostname
|
|
ddns-service hostname add myuser mypc dyn.example.com
|
|
|
|
# Add hostname with custom TTLs
|
|
ddns-service hostname add myuser mypc dyn.example.com \
|
|
--dns-ttl 60 --expiry-ttl 7200
|
|
|
|
# Modify hostname TTLs
|
|
ddns-service hostname modify mypc dyn.example.com --dns-ttl 120
|
|
|
|
# Delete hostname
|
|
ddns-service hostname delete mypc dyn.example.com
|
|
```
|
|
|
|
### Manual Cleanup
|
|
|
|
```bash
|
|
# Manually cleanup expired hostnames (delete DNS records)
|
|
ddns-service cleanup
|
|
```
|
|
|
|
### Run Daemon
|
|
|
|
```bash
|
|
ddns-service --daemon
|
|
|
|
# With debug logging
|
|
ddns-service --daemon --debug
|
|
```
|
|
|
|
## HTTP API
|
|
|
|
### Request
|
|
|
|
```
|
|
GET /update?hostname=mypc.dyn.example.com[&myip=1.2.3.4][&myip6=2001:db8::1]
|
|
Authorization: Basic base64(username:password)
|
|
```
|
|
|
|
Authentication can also be provided as query parameters:
|
|
```
|
|
GET /update?hostname=mypc.dyn.example.com&username=myuser&password=secret
|
|
```
|
|
|
|
### IP Detection
|
|
|
|
- If `myip` and/or `myip6` provided: use those values
|
|
- If neither provided: use client's source IP
|
|
- IPv4 addresses create A records
|
|
- IPv6 addresses create AAAA records
|
|
|
|
### Responses
|
|
|
|
**Plain text (default, DynDNS-compatible):**
|
|
- `good <ipv4> <ipv6>` - update successful
|
|
- `nochg <ipv4> <ipv6>` - no change needed
|
|
- `badauth` - authentication failed
|
|
- `nohost` - hostname not found or not authorized
|
|
- `dnserr` - DNS update failed
|
|
- `abuse` - rate limit exceeded
|
|
|
|
**JSON (with `Accept: application/json`):**
|
|
```json
|
|
{"status": "good", "ipv4": "1.2.3.4", "ipv6": "2001:db8::1"}
|
|
```
|
|
|
|
## Client Examples
|
|
|
|
### curl
|
|
|
|
```bash
|
|
curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com"
|
|
```
|
|
|
|
With explicit IP:
|
|
```bash
|
|
curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com&myip=1.2.3.4"
|
|
```
|
|
|
|
### wget
|
|
|
|
```bash
|
|
wget -qO- --user=username --password=password \
|
|
"https://ddns.example.com/update?hostname=mypc.dyn.example.com"
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
- **Do not expose directly to the internet** - run behind a reverse proxy (e.g. nginx, caddy, apache) that handles TLS termination
|
|
- By default the daemon binds to localhost only; configure your reverse proxy to forward requests
|
|
- Passwords are hashed with Argon2 (memory-hard, resistant to GPU attacks)
|
|
- Rate limiting protects against brute-force attacks
|
|
- Database file should have restricted permissions
|
|
- Consider fail2ban for additional protection
|
|
- **Database backups**: Create recurring backups before upgrades. Migration errors require manual recovery from backup.
|
|
|
|
## TTL Behavior
|
|
|
|
Each hostname has two TTL values:
|
|
|
|
- **dns_ttl**: TTL value set on DNS records (default: 60 seconds)
|
|
- **expiry_ttl**: Time without updates before record is removed (default: 3600 seconds)
|
|
|
|
Set `expiry_ttl = 0` to disable expiration entirely for a hostname.
|
|
|
|
When a hostname expires:
|
|
1. DNS records (A and/or AAAA) are deleted
|
|
2. Email notification is sent to the user
|
|
3. Records can be restored by sending a new update request
|
|
|
|
## Logging
|
|
|
|
The daemon supports stdout, syslog, and file logging targets.
|
|
|
|
### Configuration Options
|
|
|
|
| Option | Default | Description |
|
|
|--------|---------|-------------|
|
|
| `log_level` | INFO | DEBUG, INFO, WARNING, ERROR |
|
|
| `log_target` | stdout | stdout, syslog, or file |
|
|
| `syslog_socket` | /dev/log | Path to syslog socket (syslog only) |
|
|
| `syslog_facility` | daemon | daemon, user, local0-7 (syslog only) |
|
|
| `log_file` | /var/log/ddns-service/ddns-service.log | Log file path (file only) |
|
|
| `log_file_size` | 52428800 | Max file size before rotation in bytes (file only) |
|
|
| `log_versions` | 5 | Number of backup files to keep (file only) |
|
|
| `log_requests` | false | Log HTTP request lines at INFO level |
|