# DDNS Service 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-service/ ├── ddns-service # Main executable ├── ddns_service/ # 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-service.service ├── requirements.txt └── README.md ``` ## Installation ```bash 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-service/config.toml` or `./config.toml`: ```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.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] # 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. ```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 ``` ### 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 dyn.example.com # Add hostname with custom TTLs ./ddns-service hostname add myuser mypc.dyn.example.com 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 ./ddns-service cleanup ``` ### Run Daemon ```bash ./ddns-service --daemon # With debug logging ./ddns-service --daemon --debug ``` ### Debug Mode Use `--debug` to enable debug logging for any command: ```bash ./ddns-service --debug user list ./ddns-service --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 ` - 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" ``` ### wget ```bash wget -qO- --user=username --password=password \ "https://ddns.example.com/update?hostname=mypc.dyn.example.com" ``` ## Systemd Setup 1. Create system user: ```bash useradd -r -s /sbin/nologin ddns ``` 2. Create directories: ```bash mkdir -p /etc/ddns-service /var/lib/ddns-service chown ddns:ddns /var/lib/ddns-service ``` 3. Install files: ```bash cp -r ddns_service /opt/ddns-service/ cp ddns-service /opt/ddns-service/ cp config.example.toml /etc/ddns-service/config.toml cp ddns-service.service /etc/systemd/system/ ``` 4. Configure and start: ```bash # Edit config vi /etc/ddns-service/config.toml # Initialize database /opt/ddns-service/ddns-service --init-db # Enable and start systemctl daemon-reload systemctl enable ddns-service systemctl start ddns-service ``` ## 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-service[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