Migrate to qmcgaw/ddns-updater v0.3.0
BREAKING CHANGE: Replace custom implementation with universal DDNS client - Replace custom Python implementation with qmcgaw/ddns-updater - Add Web UI for monitoring on port 8000 - Add support for 50+ DNS providers (not just Hetzner) - Add multi-domain support via JSON configuration - Add config.json.example for Hetzner setup - Update documentation with Web UI and new setup process - Remove custom Python code, Dockerfile, requirements.txt - Simplify deployment with established, production-ready solution Benefits: - Professional Web UI for status monitoring - Active community support - Flexibility for future provider changes - Well-tested, production-ready solution Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
75efdcc16d
commit
1b7ebf1b00
|
|
@ -1,34 +0,0 @@
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
venv/
|
|
||||||
env/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
README.md
|
|
||||||
CHANGELOG.md
|
|
||||||
CLAUDE.md
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose.yml
|
|
||||||
docker-compose.override.yml
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
28
.env.example
28
.env.example
|
|
@ -1,23 +1,9 @@
|
||||||
# Hetzner DNS API Token
|
# DDNS-Updater Konfiguration
|
||||||
# Erstellen Sie einen Token unter: https://dns.hetzner.com/
|
|
||||||
HETZNER_API_TOKEN=your_api_token_here
|
|
||||||
|
|
||||||
# Domain Name (z.B. example.com)
|
# Update-Intervall (Standard: 5m)
|
||||||
DOMAIN=example.com
|
# Mögliche Werte: 30s, 1m, 5m, 10m, etc.
|
||||||
|
PERIOD=5m
|
||||||
|
|
||||||
# DNS Record Name
|
# Log Level
|
||||||
# Verwenden Sie '@' für die Root-Domain oder einen Subdomain-Namen wie 'home'
|
# Mögliche Werte: debug, info, warning, error
|
||||||
RECORD_NAME=@
|
LOG_LEVEL=info
|
||||||
|
|
||||||
# DNS Record Typ
|
|
||||||
# A für IPv4, AAAA für IPv6
|
|
||||||
RECORD_TYPE=A
|
|
||||||
|
|
||||||
# Optional: Zone ID (wird automatisch ermittelt wenn leer)
|
|
||||||
ZONE_ID=
|
|
||||||
|
|
||||||
# Prüfintervall in Sekunden (Standard: 300 = 5 Minuten)
|
|
||||||
CHECK_INTERVAL=300
|
|
||||||
|
|
||||||
# Log Level (DEBUG, INFO, WARNING, ERROR)
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# DDNS-Updater data
|
||||||
|
data/
|
||||||
|
config.json
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
|
||||||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -4,6 +4,31 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
|
||||||
|
|
||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-02-16
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- **BREAKING**: Migration von eigener Implementierung zu **qmcgaw/ddns-updater**
|
||||||
|
- Neue Konfiguration via JSON (`data/config.json`) statt Umgebungsvariablen
|
||||||
|
- Vereinfachtes Setup mit etabliertem, universellen DDNS-Client
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
- Web-UI für Überwachung und Verwaltung (Port 8000)
|
||||||
|
- Unterstützung für 50+ DNS-Provider (nicht nur Hetzner)
|
||||||
|
- Multi-Domain-Support in einer Konfiguration
|
||||||
|
- Beispiel-Konfiguration (`config.json.example`)
|
||||||
|
- Verbesserte Dokumentation mit Web-UI Hinweisen
|
||||||
|
|
||||||
|
### Entfernt
|
||||||
|
- Eigene Python-Implementierung (`dyndns.py`)
|
||||||
|
- Custom Dockerfile und requirements.txt
|
||||||
|
- .dockerignore (nicht mehr benötigt)
|
||||||
|
|
||||||
|
### Vorteile
|
||||||
|
- Etablierte, gut getestete Lösung
|
||||||
|
- Aktive Community und Support
|
||||||
|
- Flexibilität für zukünftige Provider-Wechsel
|
||||||
|
- Professionelle Web-UI zur Statusüberwachung
|
||||||
|
|
||||||
## [0.2.0] - 2026-02-16
|
## [0.2.0] - 2026-02-16
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|
|
||||||
177
CLAUDE.md
177
CLAUDE.md
|
|
@ -4,22 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a Docker-based Dynamic DNS client for the Hetzner DNS Console. It automatically detects IP changes and updates DNS records via the Hetzner DNS API.
|
This is a Docker-based Dynamic DNS client for Hetzner DNS (and 50+ other providers) using **[qmcgaw/ddns-updater](https://github.com/qdm12/ddns-updater)** - a universal, production-ready DDNS solution with web UI.
|
||||||
|
|
||||||
**Core Components:**
|
**Core Components:**
|
||||||
- `dyndns.py`: Main Python application that handles IP detection and DNS updates
|
- `docker-compose.yml`: Service configuration using qmcgaw/ddns-updater image
|
||||||
- `Dockerfile`: Container configuration for the application
|
- `data/config.json`: DDNS configuration (domains, tokens, providers)
|
||||||
- `docker-compose.yml`: Docker Compose setup for easy deployment
|
- `config.json.example`: Configuration template for Hetzner setup
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Docker Operations
|
### Docker Operations
|
||||||
|
|
||||||
**Build the container:**
|
|
||||||
```bash
|
|
||||||
docker-compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Start the service:**
|
**Start the service:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
@ -40,62 +35,92 @@ docker-compose restart
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rebuild after code changes:**
|
**Check status:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d --build
|
docker-compose ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Configuration Management
|
||||||
|
|
||||||
**Run Python script directly (requires Python 3.11+):**
|
**Create initial config:**
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
mkdir -p data
|
||||||
pip install -r requirements.txt
|
cp config.json.example data/config.json
|
||||||
|
# Edit data/config.json with your credentials
|
||||||
# Set environment variables or use .env file
|
|
||||||
export HETZNER_API_TOKEN="your_token"
|
|
||||||
export DOMAIN="example.com"
|
|
||||||
export RECORD_NAME="@"
|
|
||||||
|
|
||||||
# Run script
|
|
||||||
python dyndns.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test IP detection without updating DNS:**
|
**Validate config JSON:**
|
||||||
```python
|
```bash
|
||||||
from dyndns import HetznerDynDNS
|
cat data/config.json | jq .
|
||||||
client = HetznerDynDNS(api_token, domain, record_name)
|
|
||||||
current_ip = client.get_current_ip()
|
|
||||||
print(f"Current IP: {current_ip}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Get Hetzner Zone ID via API:**
|
||||||
|
```bash
|
||||||
|
curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web UI
|
||||||
|
|
||||||
|
**Access Web UI:**
|
||||||
|
Open browser to `http://localhost:8000`
|
||||||
|
|
||||||
|
The Web UI shows:
|
||||||
|
- Current IP status
|
||||||
|
- Last update timestamp
|
||||||
|
- Update history
|
||||||
|
- Errors and warnings
|
||||||
|
- Per-domain status
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Application Flow
|
### Application Flow
|
||||||
|
|
||||||
1. **Initialization**: Load configuration from environment variables
|
1. **Initialization**: Load configuration from `data/config.json`
|
||||||
2. **Zone Lookup**: Auto-detect Hetzner DNS Zone ID if not provided
|
2. **Main Loop**:
|
||||||
3. **Main Loop**:
|
- Get current public IP (IPv4/IPv6)
|
||||||
- Get current public IP (via api.ipify.org)
|
- Compare with DNS records for each configured domain
|
||||||
- Fetch existing DNS record from Hetzner API
|
- Update DNS if IP has changed
|
||||||
- Compare IPs and update if changed
|
- Wait for configured period (default: 5 minutes)
|
||||||
- Wait for configured interval (default: 5 minutes)
|
|
||||||
- Repeat
|
- Repeat
|
||||||
|
|
||||||
### Hetzner DNS API Integration
|
### Configuration Structure
|
||||||
|
|
||||||
The application uses Hetzner's DNS API v1 (https://dns.hetzner.com/api/v1):
|
Configuration is in JSON format at `data/config.json`:
|
||||||
- **Authentication**: Via `Auth-API-Token` header
|
|
||||||
- **GET /zones**: Find zone ID for domain
|
|
||||||
- **GET /records**: List DNS records for zone
|
|
||||||
- **POST /records**: Create new DNS record
|
|
||||||
- **PUT /records/{id}**: Update existing DNS record
|
|
||||||
|
|
||||||
### Configuration
|
```json
|
||||||
|
{
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"provider": "hetzner",
|
||||||
|
"zone_identifier": "zone_id",
|
||||||
|
"domain": "example.com",
|
||||||
|
"host": "@",
|
||||||
|
"ttl": 60,
|
||||||
|
"token": "api_token",
|
||||||
|
"ip_version": "ipv4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
All configuration is via environment variables (see `.env.example`):
|
**Key Parameters:**
|
||||||
- **Required**: `HETZNER_API_TOKEN`, `DOMAIN`
|
- `provider`: DNS provider name (hetzner, cloudflare, duckdns, etc.)
|
||||||
- **Optional**: `RECORD_NAME` (default: @), `RECORD_TYPE` (default: A), `CHECK_INTERVAL` (default: 300), `LOG_LEVEL` (default: INFO)
|
- `zone_identifier`: Hetzner DNS Zone ID
|
||||||
|
- `domain`: Base domain name
|
||||||
|
- `host`: Subdomain or `@` for root
|
||||||
|
- `ttl`: Time-to-live in seconds
|
||||||
|
- `token`: Hetzner API token with DNS edit permissions
|
||||||
|
- `ip_version`: `ipv4`, `ipv6`, or `ipv4 or ipv6`
|
||||||
|
|
||||||
|
### Multi-Domain Support
|
||||||
|
|
||||||
|
Add multiple entries in the `settings` array to manage multiple domains or subdomains.
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Optional variables in `.env`:
|
||||||
|
- `PERIOD`: Update check interval (default: 5m)
|
||||||
|
- `LOG_LEVEL`: Logging verbosity (debug, info, warning, error)
|
||||||
|
|
||||||
## Versioning Strategy
|
## Versioning Strategy
|
||||||
|
|
||||||
|
|
@ -111,37 +136,67 @@ This project uses a simplified versioning scheme:
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `dyndns.py`: Core application logic (250+ lines)
|
- `docker-compose.yml`: Service definition using qmcgaw/ddns-updater
|
||||||
- `requirements.txt`: Python dependencies
|
- `config.json.example`: Template for Hetzner configuration
|
||||||
- `Dockerfile`: Multi-stage build with non-root user
|
- `data/config.json`: Active configuration (not in git)
|
||||||
- `docker-compose.yml`: Service configuration with restart policy
|
- `.env.example`: Optional environment variables template
|
||||||
- `.env.example`: Configuration template
|
|
||||||
- `CHANGELOG.md`: Version history
|
- `CHANGELOG.md`: Version history
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
- API token is sensitive - never commit `.env` file
|
- API token is sensitive - never commit `data/config.json` or `.env`
|
||||||
- Container runs as non-root user (UID 1000)
|
- Container runs with minimal privileges
|
||||||
- Minimal Python slim image for reduced attack surface
|
- Only ports 8000 (web UI) exposed
|
||||||
|
- Use HTTPS reverse proxy for production deployments
|
||||||
- API token should have minimal required permissions (DNS read/write only)
|
- API token should have minimal required permissions (DNS read/write only)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Check if container is running:**
|
**View Web UI status:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose ps
|
# Open http://localhost:8000 in browser
|
||||||
```
|
```
|
||||||
|
|
||||||
**View recent logs:**
|
**Check container logs:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose logs --tail=50
|
docker-compose logs --tail=50 -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validate configuration:**
|
||||||
|
```bash
|
||||||
|
# Check JSON syntax
|
||||||
|
cat data/config.json | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test API connectivity:**
|
**Test API connectivity:**
|
||||||
```bash
|
```bash
|
||||||
# From host
|
# List zones
|
||||||
curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones
|
curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones
|
||||||
|
|
||||||
|
# List records for a zone
|
||||||
|
curl -H "Auth-API-Token: YOUR_TOKEN" "https://dns.hetzner.com/api/v1/records?zone_id=ZONE_ID"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Debug mode:**
|
**Debug mode:**
|
||||||
Set `LOG_LEVEL=DEBUG` in `.env` and restart container.
|
Set `LOG_LEVEL=debug` in `.env` and restart container.
|
||||||
|
|
||||||
|
**Container won't start:**
|
||||||
|
- Check `docker-compose logs`
|
||||||
|
- Verify `data/config.json` exists and is valid JSON
|
||||||
|
- Check port 8000 is not in use
|
||||||
|
|
||||||
|
## Provider Migration
|
||||||
|
|
||||||
|
To switch from Hetzner to another provider:
|
||||||
|
1. Check [supported providers](https://github.com/qdm12/ddns-updater#providers)
|
||||||
|
2. Update `provider` field in `data/config.json`
|
||||||
|
3. Adjust provider-specific fields
|
||||||
|
4. Restart: `docker-compose restart`
|
||||||
|
|
||||||
|
No code changes needed - just configuration!
|
||||||
|
|
||||||
|
## Upstream Documentation
|
||||||
|
|
||||||
|
- [DDNS-Updater GitHub](https://github.com/qdm12/ddns-updater)
|
||||||
|
- [Hetzner Configuration Guide](https://github.com/qdm12/ddns-updater/blob/master/docs/hetzner.md)
|
||||||
|
- [All Supported Providers](https://github.com/qdm12/ddns-updater#providers)
|
||||||
|
|
|
||||||
29
Dockerfile
29
Dockerfile
|
|
@ -1,29 +0,0 @@
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Copy application
|
|
||||||
COPY dyndns.py .
|
|
||||||
|
|
||||||
# Make script executable
|
|
||||||
RUN chmod +x dyndns.py
|
|
||||||
|
|
||||||
# Run as non-root user
|
|
||||||
RUN useradd -m -u 1000 dyndns && \
|
|
||||||
chown -R dyndns:dyndns /app
|
|
||||||
|
|
||||||
USER dyndns
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=30s --retries=3 \
|
|
||||||
CMD python -c "import sys; sys.exit(0)"
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
CMD ["python", "-u", "dyndns.py"]
|
|
||||||
187
README.md
187
README.md
|
|
@ -1,14 +1,18 @@
|
||||||
# DynDNS Docker für Hetzner DNS
|
# DynDNS Docker für Hetzner DNS
|
||||||
|
|
||||||
Ein Docker-basiertes Dynamic DNS System für die Hetzner DNS Console.
|
Ein Docker-basiertes Dynamic DNS System für die Hetzner DNS Console mit **[qmcgaw/ddns-updater](https://github.com/qdm12/ddns-updater)** - einem universellen DDNS-Client, der 50+ Provider unterstützt.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Automatische Erkennung von IP-Änderungen
|
- ✅ Automatische Erkennung von IP-Änderungen
|
||||||
- Integration mit Hetzner DNS API
|
- ✅ Integration mit Hetzner DNS API
|
||||||
- Konfigurierbare Check-Intervalle
|
- ✅ Web-UI für Überwachung und Verwaltung (Port 8000)
|
||||||
- Docker-Container für einfaches Deployment
|
- ✅ Konfigurierbare Check-Intervalle
|
||||||
- Umgebungsvariablen für sichere Konfiguration
|
- ✅ Unterstützt 50+ DNS-Provider (nicht nur Hetzner)
|
||||||
|
- ✅ Multi-Domain-Support
|
||||||
|
- ✅ IPv4 und IPv6 Unterstützung
|
||||||
|
- ✅ Health Check für Container-Monitoring
|
||||||
|
- ✅ Automatische Restart-Policy
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
|
|
@ -25,43 +29,125 @@ git clone <repository-url>
|
||||||
cd dyndns-docker
|
cd dyndns-docker
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Konfiguration
|
### 2. Konfiguration erstellen
|
||||||
|
|
||||||
Kopieren Sie die Beispiel-Konfigurationsdatei und passen Sie sie an:
|
#### a) Umgebungsvariablen (optional)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Bearbeiten Sie die `.env` Datei mit Ihren Daten:
|
Bearbeiten Sie `.env` für optionale Einstellungen:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
HETZNER_API_TOKEN=your_api_token_here
|
PERIOD=5m # Update-Intervall
|
||||||
DOMAIN=example.com
|
LOG_LEVEL=info # Log-Level
|
||||||
RECORD_NAME=home
|
|
||||||
RECORD_TYPE=A
|
|
||||||
CHECK_INTERVAL=300
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Container starten
|
#### b) DDNS-Konfiguration (erforderlich)
|
||||||
|
|
||||||
|
Erstellen Sie `data/config.json` basierend auf der Vorlage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
cp config.json.example data/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Bearbeiten Sie `data/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"provider": "hetzner",
|
||||||
|
"zone_identifier": "your_zone_id_here",
|
||||||
|
"domain": "example.com",
|
||||||
|
"host": "@",
|
||||||
|
"ttl": 60,
|
||||||
|
"token": "your_hetzner_api_token_here",
|
||||||
|
"ip_version": "ipv4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Hetzner API Token und Zone ID ermitteln
|
||||||
|
|
||||||
|
#### API Token erstellen:
|
||||||
|
1. Melden Sie sich im [Hetzner DNS Console](https://dns.hetzner.com/) an
|
||||||
|
2. Gehen Sie zu **"API Tokens"**
|
||||||
|
3. Klicken Sie auf **"Create access token"**
|
||||||
|
4. Geben Sie einen Namen ein (z.B. "DynDNS")
|
||||||
|
5. Wählen Sie **Read & Write** Rechte
|
||||||
|
6. Kopieren Sie den generierten Token
|
||||||
|
|
||||||
|
#### Zone ID finden:
|
||||||
|
1. Gehen Sie zu Ihrer Domain-Übersicht
|
||||||
|
2. Die **Zone ID** finden Sie in der URL oder im Domain-Detail
|
||||||
|
3. Alternativ: API-Aufruf `curl -H "Auth-API-Token: YOUR_TOKEN" https://dns.hetzner.com/api/v1/zones`
|
||||||
|
|
||||||
|
### 4. Container starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 5. Web-UI aufrufen
|
||||||
|
|
||||||
|
Öffnen Sie im Browser: **http://localhost:8000**
|
||||||
|
|
||||||
|
Hier sehen Sie:
|
||||||
|
- ✅ Aktuellen IP-Status
|
||||||
|
- ✅ Letztes Update
|
||||||
|
- ✅ Update-Historie
|
||||||
|
- ✅ Fehler und Warnungen
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
|
### Domain-Konfiguration
|
||||||
|
|
||||||
|
| Parameter | Beschreibung | Beispiel |
|
||||||
|
|-----------|-------------|----------|
|
||||||
|
| `provider` | DNS-Provider Name | `hetzner` |
|
||||||
|
| `zone_identifier` | Hetzner Zone ID | `abc123...` |
|
||||||
|
| `domain` | Domain-Name | `example.com` |
|
||||||
|
| `host` | Subdomain oder `@` für Root | `@`, `home`, `*.example.com` |
|
||||||
|
| `ttl` | Time-to-Live in Sekunden | `60` |
|
||||||
|
| `token` | Hetzner API Token | `xyz789...` |
|
||||||
|
| `ip_version` | IP-Version | `ipv4`, `ipv6`, `ipv4 or ipv6` |
|
||||||
|
|
||||||
### Umgebungsvariablen
|
### Umgebungsvariablen
|
||||||
|
|
||||||
| Variable | Beschreibung | Beispiel |
|
| Variable | Beschreibung | Standard |
|
||||||
|----------|-------------|----------|
|
|----------|-------------|----------|
|
||||||
| `HETZNER_API_TOKEN` | Hetzner DNS API Token | `abc123...` |
|
| `PERIOD` | Update-Intervall | `5m` |
|
||||||
| `ZONE_ID` | Hetzner DNS Zone ID (optional, wird automatisch ermittelt) | `xyz789...` |
|
| `LOG_LEVEL` | Log-Level | `info` |
|
||||||
| `DOMAIN` | Domain-Name | `example.com` |
|
|
||||||
| `RECORD_NAME` | DNS Record Name | `home` oder `@` für Root |
|
### Mehrere Domains konfigurieren
|
||||||
| `RECORD_TYPE` | DNS Record Typ | `A` (IPv4) oder `AAAA` (IPv6) |
|
|
||||||
| `CHECK_INTERVAL` | Prüfintervall in Sekunden | `300` (5 Minuten) |
|
Sie können mehrere Domains in `data/config.json` hinzufügen:
|
||||||
| `LOG_LEVEL` | Log-Level | `INFO`, `DEBUG`, `WARNING` |
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"provider": "hetzner",
|
||||||
|
"zone_identifier": "zone1_id",
|
||||||
|
"domain": "example.com",
|
||||||
|
"host": "@",
|
||||||
|
"token": "token1",
|
||||||
|
"ip_version": "ipv4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "hetzner",
|
||||||
|
"zone_identifier": "zone2_id",
|
||||||
|
"domain": "example.org",
|
||||||
|
"host": "home",
|
||||||
|
"token": "token2",
|
||||||
|
"ip_version": "ipv4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
|
|
||||||
|
|
@ -89,12 +175,49 @@ docker-compose restart
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
## Hetzner DNS API Token erstellen
|
### Konfiguration neu laden
|
||||||
|
|
||||||
1. Melden Sie sich im [Hetzner DNS Console](https://dns.hetzner.com/) an
|
Nach Änderungen an `data/config.json`:
|
||||||
2. Gehen Sie zu "API Tokens"
|
|
||||||
3. Erstellen Sie einen neuen API Token mit Lese- und Schreibrechten
|
```bash
|
||||||
4. Kopieren Sie den Token in Ihre `.env` Datei
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Weitere unterstützte Provider
|
||||||
|
|
||||||
|
Dieser Setup nutzt **qmcgaw/ddns-updater**, der auch diese Provider unterstützt:
|
||||||
|
|
||||||
|
- Cloudflare
|
||||||
|
- Google Domains
|
||||||
|
- DuckDNS
|
||||||
|
- No-IP
|
||||||
|
- Namecheap
|
||||||
|
- GoDaddy
|
||||||
|
- **und 40+ weitere**
|
||||||
|
|
||||||
|
Um einen anderen Provider zu nutzen, passen Sie einfach die `provider`-Konfiguration in `data/config.json` an. Siehe [Dokumentation](https://github.com/qdm12/ddns-updater) für Details.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container startet nicht
|
||||||
|
```bash
|
||||||
|
docker-compose logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web-UI nicht erreichbar
|
||||||
|
- Prüfen Sie, ob Port 8000 verfügbar ist
|
||||||
|
- Prüfen Sie Firewall-Einstellungen
|
||||||
|
|
||||||
|
### DNS-Update schlägt fehl
|
||||||
|
- Prüfen Sie den API Token
|
||||||
|
- Prüfen Sie die Zone ID
|
||||||
|
- Prüfen Sie die Logs: `docker-compose logs -f`
|
||||||
|
|
||||||
|
### Konfiguration testen
|
||||||
|
```bash
|
||||||
|
# Prüfen Sie die config.json Syntax
|
||||||
|
cat data/config.json | jq .
|
||||||
|
```
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|
@ -103,6 +226,12 @@ Dieses Projekt folgt einer vereinfachten Versionierungsstruktur:
|
||||||
- **0.0.1**: Kleine Änderungen (Bugfixes, kleinere Verbesserungen)
|
- **0.0.1**: Kleine Änderungen (Bugfixes, kleinere Verbesserungen)
|
||||||
- **1.x**: Major Releases (nur nach Anweisung)
|
- **1.x**: Major Releases (nur nach Anweisung)
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [DDNS-Updater GitHub](https://github.com/qdm12/ddns-updater)
|
||||||
|
- [Hetzner DNS Console](https://dns.hetzner.com/)
|
||||||
|
- [Hetzner DNS API Dokumentation](https://dns.hetzner.com/api-docs)
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"provider": "hetzner",
|
||||||
|
"zone_identifier": "your_zone_id_here",
|
||||||
|
"domain": "example.com",
|
||||||
|
"host": "@",
|
||||||
|
"ttl": 60,
|
||||||
|
"token": "your_hetzner_api_token_here",
|
||||||
|
"ip_version": "ipv4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dyndns:
|
ddns-updater:
|
||||||
build: .
|
image: qmcgaw/ddns-updater:latest
|
||||||
container_name: hetzner-dyndns
|
container_name: hetzner-dyndns
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
ports:
|
||||||
- .env
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/updater/data
|
||||||
environment:
|
environment:
|
||||||
- HETZNER_API_TOKEN=${HETZNER_API_TOKEN}
|
- PERIOD=${PERIOD:-5m}
|
||||||
- DOMAIN=${DOMAIN}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
- RECORD_NAME=${RECORD_NAME:-@}
|
- TZ=Europe/Berlin
|
||||||
- RECORD_TYPE=${RECORD_TYPE:-A}
|
healthcheck:
|
||||||
- ZONE_ID=${ZONE_ID:-}
|
test: ["CMD", "wget", "-qO-", "http://localhost:8000/"]
|
||||||
- CHECK_INTERVAL=${CHECK_INTERVAL:-300}
|
interval: 1m
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|
|
||||||
266
dyndns.py
266
dyndns.py
|
|
@ -1,266 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
DynDNS Client for Hetzner DNS API
|
|
||||||
Automatically updates DNS records when IP address changes
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
from typing import Optional, Dict, Any
|
|
||||||
|
|
||||||
|
|
||||||
class HetznerDynDNS:
|
|
||||||
"""Hetzner DNS API client for dynamic DNS updates"""
|
|
||||||
|
|
||||||
BASE_URL = "https://dns.hetzner.com/api/v1"
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
api_token: str,
|
|
||||||
domain: str,
|
|
||||||
record_name: str,
|
|
||||||
record_type: str = "A",
|
|
||||||
zone_id: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initialize Hetzner DynDNS client
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_token: Hetzner DNS API token
|
|
||||||
domain: Domain name (e.g., example.com)
|
|
||||||
record_name: DNS record name (e.g., 'home' or '@' for root)
|
|
||||||
record_type: Record type ('A' for IPv4, 'AAAA' for IPv6)
|
|
||||||
zone_id: Optional zone ID (will be auto-detected if not provided)
|
|
||||||
"""
|
|
||||||
self.api_token = api_token
|
|
||||||
self.domain = domain
|
|
||||||
self.record_name = record_name if record_name != "@" else ""
|
|
||||||
self.record_type = record_type
|
|
||||||
self.zone_id = zone_id
|
|
||||||
self.headers = {
|
|
||||||
"Auth-API-Token": api_token,
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
if not self.zone_id:
|
|
||||||
self.zone_id = self._get_zone_id()
|
|
||||||
|
|
||||||
def _make_request(
|
|
||||||
self,
|
|
||||||
method: str,
|
|
||||||
endpoint: str,
|
|
||||||
data: Optional[Dict[str, Any]] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Make HTTP request to Hetzner API"""
|
|
||||||
url = f"{self.BASE_URL}/{endpoint}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if method == "GET":
|
|
||||||
response = requests.get(url, headers=self.headers)
|
|
||||||
elif method == "POST":
|
|
||||||
response = requests.post(url, headers=self.headers, json=data)
|
|
||||||
elif method == "PUT":
|
|
||||||
response = requests.put(url, headers=self.headers, json=data)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
||||||
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error(f"API request failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _get_zone_id(self) -> str:
|
|
||||||
"""Get zone ID for the domain"""
|
|
||||||
logging.info(f"Looking up zone ID for domain: {self.domain}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self._make_request("GET", "zones")
|
|
||||||
zones = response.get("zones", [])
|
|
||||||
|
|
||||||
for zone in zones:
|
|
||||||
if zone.get("name") == self.domain:
|
|
||||||
zone_id = zone.get("id")
|
|
||||||
logging.info(f"Found zone ID: {zone_id}")
|
|
||||||
return zone_id
|
|
||||||
|
|
||||||
raise ValueError(f"Zone not found for domain: {self.domain}")
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to get zone ID: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_current_ip(self) -> str:
|
|
||||||
"""Get current public IP address"""
|
|
||||||
try:
|
|
||||||
if self.record_type == "A":
|
|
||||||
# IPv4
|
|
||||||
response = requests.get("https://api.ipify.org", timeout=10)
|
|
||||||
else:
|
|
||||||
# IPv6
|
|
||||||
response = requests.get("https://api6.ipify.org", timeout=10)
|
|
||||||
|
|
||||||
response.raise_for_status()
|
|
||||||
ip = response.text.strip()
|
|
||||||
logging.debug(f"Current IP ({self.record_type}): {ip}")
|
|
||||||
return ip
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logging.error(f"Failed to get current IP: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_dns_record(self) -> Optional[Dict[str, Any]]:
|
|
||||||
"""Get existing DNS record"""
|
|
||||||
try:
|
|
||||||
response = self._make_request("GET", f"records?zone_id={self.zone_id}")
|
|
||||||
records = response.get("records", [])
|
|
||||||
|
|
||||||
for record in records:
|
|
||||||
if (record.get("type") == self.record_type and
|
|
||||||
record.get("name") == self.record_name):
|
|
||||||
logging.debug(f"Found existing record: {record}")
|
|
||||||
return record
|
|
||||||
|
|
||||||
logging.info(f"No existing record found for {self.record_name}.{self.domain}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to get DNS record: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def create_dns_record(self, ip: str) -> Dict[str, Any]:
|
|
||||||
"""Create new DNS record"""
|
|
||||||
logging.info(f"Creating new DNS record: {self.record_name}.{self.domain} -> {ip}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"zone_id": self.zone_id,
|
|
||||||
"type": self.record_type,
|
|
||||||
"name": self.record_name,
|
|
||||||
"value": ip,
|
|
||||||
"ttl": 60
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self._make_request("POST", "records", data)
|
|
||||||
logging.info(f"DNS record created successfully")
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to create DNS record: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def update_dns_record(self, record_id: str, ip: str) -> Dict[str, Any]:
|
|
||||||
"""Update existing DNS record"""
|
|
||||||
logging.info(f"Updating DNS record: {self.record_name}.{self.domain} -> {ip}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"zone_id": self.zone_id,
|
|
||||||
"type": self.record_type,
|
|
||||||
"name": self.record_name,
|
|
||||||
"value": ip,
|
|
||||||
"ttl": 60
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self._make_request("PUT", f"records/{record_id}", data)
|
|
||||||
logging.info(f"DNS record updated successfully")
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to update DNS record: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def update(self) -> bool:
|
|
||||||
"""
|
|
||||||
Update DNS record if IP has changed
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if record was updated, False if no update was needed
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
current_ip = self.get_current_ip()
|
|
||||||
existing_record = self.get_dns_record()
|
|
||||||
|
|
||||||
if existing_record:
|
|
||||||
if existing_record.get("value") == current_ip:
|
|
||||||
logging.info(f"IP unchanged ({current_ip}), no update needed")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logging.info(f"IP changed: {existing_record.get('value')} -> {current_ip}")
|
|
||||||
self.update_dns_record(existing_record.get("id"), current_ip)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logging.info(f"No existing record, creating new one with IP: {current_ip}")
|
|
||||||
self.create_dns_record(current_ip)
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Update failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(log_level: str = "INFO") -> None:
|
|
||||||
"""Configure logging"""
|
|
||||||
logging.basicConfig(
|
|
||||||
level=getattr(logging, log_level.upper()),
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point"""
|
|
||||||
# Load configuration from environment variables
|
|
||||||
api_token = os.getenv("HETZNER_API_TOKEN")
|
|
||||||
domain = os.getenv("DOMAIN")
|
|
||||||
record_name = os.getenv("RECORD_NAME", "@")
|
|
||||||
record_type = os.getenv("RECORD_TYPE", "A")
|
|
||||||
zone_id = os.getenv("ZONE_ID")
|
|
||||||
check_interval = int(os.getenv("CHECK_INTERVAL", "300"))
|
|
||||||
log_level = os.getenv("LOG_LEVEL", "INFO")
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
setup_logging(log_level)
|
|
||||||
|
|
||||||
# Validate required configuration
|
|
||||||
if not api_token:
|
|
||||||
logging.error("HETZNER_API_TOKEN environment variable is required")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not domain:
|
|
||||||
logging.error("DOMAIN environment variable is required")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
logging.info("Starting Hetzner DynDNS client")
|
|
||||||
logging.info(f"Domain: {domain}")
|
|
||||||
logging.info(f"Record: {record_name if record_name else '@'}.{domain}")
|
|
||||||
logging.info(f"Type: {record_type}")
|
|
||||||
logging.info(f"Check interval: {check_interval} seconds")
|
|
||||||
|
|
||||||
# Initialize client
|
|
||||||
try:
|
|
||||||
client = HetznerDynDNS(
|
|
||||||
api_token=api_token,
|
|
||||||
domain=domain,
|
|
||||||
record_name=record_name,
|
|
||||||
record_type=record_type,
|
|
||||||
zone_id=zone_id
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to initialize client: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Main loop
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
client.update()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logging.info("Shutting down...")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Unexpected error: {e}")
|
|
||||||
|
|
||||||
# Wait before next check
|
|
||||||
time.sleep(check_interval)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
requests==2.31.0
|
|
||||||
Loading…
Reference in New Issue