Integrate DDNS registration and IP update into installer
Unit Tests / test (push) Failing after 8m57s
Unit Tests / test (push) Failing after 8m57s
setup_cell.py: register_with_ddns() called at end of setup — detects public IP via api.ipify.org, generates TOTP code from DDNS_TOTP_SECRET, POSTs to DDNS /register, saves token to data/api/.ddns_token (mode 600). Idempotent: skips if token file already exists. Fails gracefully if DDNS_TOTP_SECRET is unset or network is unreachable. scripts/ddns_update.py: standalone script for periodic IP updates. Reads token from data/api/.ddns_token, fetches current public IP, compares to cached last IP (data/api/.ddns_last_ip) and calls /update only when the IP has actually changed. Makefile: add ddns-update (run update script) and ddns-register (force re-registration by removing old token then calling register_with_ddns). Usage: DDNS_TOTP_SECRET=<secret> make ddns-register Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -238,6 +238,74 @@ def ensure_session_secret():
|
||||
print('[CREATED] data/api/.session_secret')
|
||||
|
||||
|
||||
DDNS_URL = os.environ.get('DDNS_URL', 'https://ddns.pic.ngo/api/v1')
|
||||
DDNS_TOTP_SECRET = os.environ.get('DDNS_TOTP_SECRET', '')
|
||||
|
||||
|
||||
def register_with_ddns(cell_name: str) -> None:
|
||||
"""Register cell_name.pic.ngo with the DDNS server using TOTP auth.
|
||||
|
||||
Idempotent: if a token file already exists the registration is skipped.
|
||||
Skipped silently if DDNS_TOTP_SECRET is not set.
|
||||
"""
|
||||
token_path = os.path.join(ROOT, 'data', 'api', '.ddns_token')
|
||||
if os.path.exists(token_path):
|
||||
print('[EXISTS] DDNS registration — token already present')
|
||||
return
|
||||
|
||||
if not DDNS_TOTP_SECRET:
|
||||
print('[SKIP] DDNS_TOTP_SECRET not set — skipping DDNS registration')
|
||||
return
|
||||
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# Detect public IP
|
||||
try:
|
||||
public_ip = urllib.request.urlopen(
|
||||
'https://api.ipify.org', timeout=5
|
||||
).read().decode().strip()
|
||||
except Exception as e:
|
||||
print(f'[WARN] Could not detect public IP: {e} — skipping DDNS registration')
|
||||
return
|
||||
|
||||
# Generate TOTP code (requires pyotp; if not available fall back gracefully)
|
||||
try:
|
||||
import pyotp
|
||||
otp = pyotp.TOTP(DDNS_TOTP_SECRET).now()
|
||||
except ImportError:
|
||||
# Try python3 -c as a subprocess fallback
|
||||
try:
|
||||
otp = subprocess.check_output(
|
||||
['python3', '-c', f"import pyotp; print(pyotp.TOTP('{DDNS_TOTP_SECRET}').now())"]
|
||||
).decode().strip()
|
||||
except Exception as e:
|
||||
print(f'[WARN] pyotp not available and fallback failed: {e} — skipping DDNS')
|
||||
return
|
||||
|
||||
data = json.dumps({'name': cell_name, 'ip': public_ip}).encode()
|
||||
req = urllib.request.Request(
|
||||
f'{DDNS_URL}/register',
|
||||
data=data,
|
||||
headers={'Content-Type': 'application/json', 'X-Register-OTP': otp},
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
result = json.loads(resp.read())
|
||||
token = result['token']
|
||||
os.makedirs(os.path.dirname(token_path), exist_ok=True)
|
||||
with open(token_path, 'w') as f:
|
||||
f.write(token)
|
||||
os.chmod(token_path, 0o600)
|
||||
print(f'[CREATED] DDNS registration: {result["subdomain"]} ip={public_ip}')
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f'[WARN] DDNS registration failed ({e.code}): {body}')
|
||||
except Exception as e:
|
||||
print(f'[WARN] DDNS registration failed: {e}')
|
||||
|
||||
|
||||
def bootstrap_admin_password():
|
||||
import secrets as _secrets
|
||||
users_file = os.path.join(ROOT, 'data', 'api', 'auth_users.json')
|
||||
@@ -303,6 +371,7 @@ def main():
|
||||
write_caddy_config(ip_range, cell_name, domain)
|
||||
ensure_session_secret()
|
||||
bootstrap_admin_password()
|
||||
register_with_ddns(cell_name)
|
||||
|
||||
print()
|
||||
print('--- Setup complete! Run: make start ---')
|
||||
|
||||
Reference in New Issue
Block a user