Initial commit
This commit is contained in:
364
README.md
Normal file
364
README.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# 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
|
||||
|
||||
```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-daemon/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-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.
|
||||
|
||||
```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 address | myip, ipv4, ip4 |
|
||||
| IPv6 address | myip6, ipv6, ip6 |
|
||||
| Username | username, user |
|
||||
| Password | password, pass, token |
|
||||
|
||||
## CLI Usage
|
||||
|
||||
### Initialize Database
|
||||
|
||||
```bash
|
||||
./ddns-daemon --init-db
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
./ddns-daemon cleanup
|
||||
```
|
||||
|
||||
### Run Daemon
|
||||
|
||||
```bash
|
||||
./ddns-daemon --daemon
|
||||
|
||||
# With debug logging
|
||||
./ddns-daemon --daemon --debug
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Use `--debug` to enable debug logging for any command:
|
||||
|
||||
```bash
|
||||
./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`):**
|
||||
```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-daemon /var/lib/ddns-daemon
|
||||
chown ddns:ddns /var/lib/ddns-daemon
|
||||
```
|
||||
|
||||
3. Install files:
|
||||
```bash
|
||||
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/
|
||||
```
|
||||
|
||||
4. Configure and start:
|
||||
```bash
|
||||
# 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
|
||||
Reference in New Issue
Block a user