# 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 and templates: ```bash wget -O /etc/ddns-service/config.toml https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/config.example.toml wget -O /etc/ddns-service/config.toml https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/change_notification.j2 wget -O /etc/ddns-service/config.toml https://git.ccc-rheintal.ch/spacefreak/ddns-service/raw/branch/master/files/expiry_notification.j2 # The config file must not be world readable! chmod 640 /etc/ddns-service/config.toml chown -R ddns:ddns /etc/ddns-service/ ``` 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. Set an empty list of names to disable a parameter. ```toml [[endpoints]] path = "/update" [endpoints.params] hostname = ["hostname", "host"] ipv4 = ["myip", "ipv4", "ip4"] ipv6 = ["myip6", "ipv6", "ip6"] username = ["username", "user"] password = ["password", "pass", "token"] notify_change = ["notify_change"] [[endpoints]] path = "/nic/update" [endpoints.params] hostname = ["hostname"] ipv4 = ["myip"] ipv6 = ["myip6"] username = ["username"] password = ["password"] notify_change = [] ``` **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 | | notify_change | notify_change | ## 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][¬ify_change=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 ``` Set `notify_change=1` to receive an email notification when the IP address changes. Requires email to be enabled and a change notification template configured. ### 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 ` - update successful - `nochg ` - 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" ``` With change notification: ```bash curl -u "username:password" "https://ddns.example.com/update?hostname=mypc.dyn.example.com¬ify_change=1" ``` ### 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 (if configured) 3. Records can be restored by sending a new update request ## Email Notifications Two types of email notifications are supported: - **Change notifications**: Sent when IP address changes, if client includes `notify_change=1` parameter - **Expiry notifications**: Sent automatically when hostname expires due to inactivity ### Configuration Enable email and configure templates in `[email]` section: ```toml [email] enabled = true smtp_host = "localhost" from_address = "ddns@example.com" change_notification_template = "/etc/ddns-service/change_notification.j2" expiry_notification_template = "/etc/ddns-service/expiry_notification.j2" ``` Example templates are provided in the `files/` directory. ### Template Variables Templates use Jinja2 syntax. Available variables: **Change notification:** | Variable | Description | |----------|-------------| | hostname | Hostname (without zone) | | zone | DNS zone | | fqdn | FQDN (hostname.zone) | | ipv4_changed | Boolean, IPv4 changed | | ipv4 | Current IPv4 address | | last_ipv4_update | Last IPv4 update time | | ipv6_changed | Boolean, IPv6 changed | | ipv6 | Current IPv6 address | | last_ipv6_update | Last IPv6 update time | | expiry_ttl | Expiry TTL in seconds | **Expiry notification:** | Variable | Description | |----------|-------------| | hostname | Hostname (without zone) | | zone | DNS zone | | fqdn | FQDN (hostname.zone) | | ipv4_expired | Boolean, IPv4 expired | | last_ipv4 | Last IPv4 address | | last_ipv4_update | Last IPv4 update time | | ipv6_expired | Boolean, IPv6 expired | | last_ipv6 | Last IPv6 address | | last_ipv6_update | Last IPv6 update time | | expiry_ttl | Expiry TTL in seconds | ## 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 |