Files
ddns-service/README.md
2026-01-18 06:21:46 +01:00

9.4 KiB

DDNS Daemon

Dynamic DNS update service with CLI administration. Accepts HTTP(S) requests to update DNS A/AAAA records using the dns-manager library.

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 support with transaction IDs for request tracking
  • Systemd integration
  • Content negotiation (plain text / JSON responses)
  • IDN/punycode hostname support

Project Structure

ddns-daemon/
├── ddns-daemon              # Main executable
├── ddns_daemon/             # Python package
│   ├── __init__.py          # Version info
│   ├── cleanup.py           # TTL expiry cleanup
│   ├── cli.py               # CLI commands
│   ├── config.py            # Configuration loading
│   ├── dns.py               # DNS operations
│   ├── email.py             # Email notifications
│   ├── logging.py           # Centralized logging
│   ├── models.py            # Database models
│   ├── ratelimit.py         # Rate limiting
│   ├── server.py            # HTTP server
│   └── validation.py        # Hostname validation
├── config.example.toml
├── ddns-daemon.service
├── requirements.txt
└── README.md

Installation

pip install -r requirements.txt

Dependencies

  • Python 3.11+
  • dns-manager
  • peewee
  • argon2-cffi
  • pymysql (for MariaDB support)

Configuration

Copy config.example.toml to /etc/ddns-daemon/config.toml or ./config.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-daemon.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-daemon/cert.pem"  # required if ssl = true
ssl_key_file = "/etc/ddns-daemon/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-daemon/ddns.db"  # required for sqlite

[dns_service]
# manager_config_file = "/etc/dns-manager/config.yml"  # default
# cleanup_interval = 60  # default: 60 (seconds, expired records cleanup)

[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

[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)

Endpoints

Configure one or more HTTP endpoints. If no endpoints are defined, a default endpoint at /update is created with standard parameter names.

[[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

./ddns-daemon --init-db

User Management

# List users
./ddns-daemon user list

# Add user (prompts for password)
./ddns-daemon user add myuser user@example.com

# Delete user (fails if hostnames exist)
./ddns-daemon user delete myuser

# Change password
./ddns-daemon user passwd myuser

# Update email
./ddns-daemon user email myuser new@example.com

Hostname Management

# List all hostnames
./ddns-daemon hostname list

# List hostnames for specific user
./ddns-daemon hostname list --user myuser

# Add hostname
./ddns-daemon hostname add myuser mypc.dyn.example.com dyn.example.com

# Add hostname with custom TTLs
./ddns-daemon hostname add myuser mypc.dyn.example.com dyn.example.com \
    --dns-ttl 60 --expiry-ttl 7200

# Modify hostname TTLs
./ddns-daemon hostname modify mypc.dyn.example.com --dns-ttl 120

# Delete hostname
./ddns-daemon hostname delete mypc.dyn.example.com

Manual Cleanup

./ddns-daemon cleanup

Run Daemon

./ddns-daemon --daemon

# With debug logging
./ddns-daemon --daemon --debug

Debug Mode

Use --debug to enable debug logging for any command:

./ddns-daemon --debug user list
./ddns-daemon --debug cleanup

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 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):

{"status": "good", "ipv4": "1.2.3.4", "ipv6": "2001:db8::1"}

Client Examples

curl

curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com"

With explicit IP:

curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com&myip=1.2.3.4"

wget

wget -qO- --user=username --password=password \
    "https://ddns.example.com/update?hostname=mypc.dyn.example.com"

Systemd Setup

  1. Create system user:
useradd -r -s /sbin/nologin ddns
  1. Create directories:
mkdir -p /etc/ddns-daemon /var/lib/ddns-daemon
chown ddns:ddns /var/lib/ddns-daemon
  1. Install files:
cp -r ddns_daemon /opt/ddns-daemon/
cp ddns-daemon /opt/ddns-daemon/
cp config.example.toml /etc/ddns-daemon/config.toml
cp ddns-daemon.service /etc/systemd/system/
  1. Configure and start:
# Edit config
vi /etc/ddns-daemon/config.toml

# Initialize database
/opt/ddns-daemon/ddns_daemon.py --init-db

# Enable and start
systemctl daemon-reload
systemctl enable ddns-daemon
systemctl start ddns-daemon

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

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 and syslog logging targets.

Configuration Options

Option Default Description
log_level INFO DEBUG, INFO, WARNING, ERROR
log_target stdout stdout or syslog
syslog_socket /dev/log Path to syslog socket
syslog_facility daemon daemon, user, local0-7
log_requests false Log HTTP request lines at INFO level

Transaction IDs

Each HTTP request is assigned a random 8-character transaction ID for log correlation. All log messages during request processing include this ID:

2025-01-17 12:34:56 [INFO] [a1b2c3d4] Updated: hostname=mypc.dyn.example.com ipv4=1.2.3.4 ipv6=N/A

Syslog Format

When using syslog, timestamps are omitted (syslog provides them):

ddns-daemon[12345]: [INFO] [a1b2c3d4] Updated: hostname=mypc.dyn.example.com ipv4=1.2.3.4 ipv6=N/A

CLI Logging

CLI commands run silently by default. Use --debug to enable logging output.

License

GPL-3.0