diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1ca0f89..7c1669a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,9 @@ jobs: - name: test_queue.sh run: bash hosting-panels/common/tests/test_queue.sh + - name: test_tsig.sh + run: bash hosting-panels/common/tests/test_tsig.sh + - name: test_valid_ip.sh run: bash hosting-panels/common/tests/test_valid_ip.sh diff --git a/hosting-panels/common/seconddns-queued b/hosting-panels/common/seconddns-queued index 8b9e9aa..1118232 100755 --- a/hosting-panels/common/seconddns-queued +++ b/hosting-panels/common/seconddns-queued @@ -92,6 +92,7 @@ def load_config(): "api_url": api_url.rstrip("/"), "api_key": api_key, "delete_check_master_ip": cp.getboolean("seconddns", "delete_check_master_ip", fallback=True), + "tsig_key_id": cp.get("tsig", "key_id", fallback="").strip(), "poll_interval": cp.getint("queue", "poll_interval", fallback=60), "http_timeout": cp.getint("queue", "http_timeout", fallback=15), "backoff_min": cp.getint("queue", "backoff_min", fallback=30), @@ -226,8 +227,10 @@ def detail(code, data): def send_op(cfg, op, domain, master_ip): """Deliver one operation. Returns (DONE / RETRY / FAILED, detail string).""" if op == "create": - code, data = api(cfg, "POST", "/api/zones", - {"name": domain, "masterIp": master_ip}) + payload = {"name": domain, "masterIp": master_ip} + if cfg["tsig_key_id"]: + payload["tsigKeyId"] = cfg["tsig_key_id"] + code, data = api(cfg, "POST", "/api/zones", payload) if code in (200, 201): return DONE, detail(code, data) if code == 409: @@ -243,6 +246,8 @@ def send_op(cfg, op, domain, master_ip): log(f"[~] worker: zone {domain} already exists (treated as success)") return DONE, detail(code, data) if code in (400, 422): + if code == 422 and cfg["tsig_key_id"]: + log(f"[!] worker: zone {domain} rejected with TSIG key {cfg['tsig_key_id']} — key deleted from the account? rerun the installer with the new key") return FAILED, detail(code, data) return RETRY, detail(code, data) diff --git a/hosting-panels/common/seconddns-tsig b/hosting-panels/common/seconddns-tsig new file mode 100755 index 0000000..99c572b --- /dev/null +++ b/hosting-panels/common/seconddns-tsig @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# Copyright © 2025-2026 SecondDNS +# Licensed under GNU General Public License v3.0 or SecondDNS Commercial License +# See LICENSE (GPLv3) or LICENSE.COMMERCIAL (commercial) for details +"""TSIG key of this panel server, for the installers. + + seconddns-tsig resolve --tsig=[ALGORITHM:]KEY:SECRET [--write] + find KEY in the account by its label or full name (as in Settings › DNS), check + the secret against it and print "id name algorithm"; --write stores them in the + [tsig] section, and the secret in a root-only file + seconddns-tsig status the key this server uses, and whether the + account still has it + seconddns-tsig check-secret ALGORITHM SECRET exit 0 when the secret fits the algorithm + seconddns-tsig secret print the stored secret (root) + seconddns-tsig clear remove the [tsig] section and the secret file + +The config is world-readable (panel hooks read it as non-root users), so the secret lives in +/etc/seconddns/tsig.secret (0600) and the config only names that file. It is kept so the key +can be put back on the panel's DNS server when the panel rewrites its configuration. +With a [tsig] section, seconddns-queued creates zones with that key (tsigKeyId). +""" +import argparse +import configparser +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) +from seconddns_common import load_config, resolve_tsig_key, check_tsig_secret, parse_tsig_pair # noqa: E402 + + +def write_section(path, body): + """Replace or drop the [tsig] section, leaving the rest of the file as it is.""" + with open(path) as f: + text = f.read() + text = re.sub(r"(?ms)^\[tsig\]\n.*?(?=^\[|\Z)", "", text).rstrip("\n") + "\n" + if body: + text += "\n[tsig]\n" + body + tmp = path + ".tmp" + with open(tmp, "w") as f: + f.write(text) + os.chmod(tmp, os.stat(path).st_mode & 0o777) + os.replace(tmp, path) + + +SECRET_FILE = os.environ.get("SECONDDNS_TSIG_SECRET", "/etc/seconddns/tsig.secret") + + +def write_secret(secret): + os.makedirs(os.path.dirname(SECRET_FILE), mode=0o700, exist_ok=True) + fd = os.open(SECRET_FILE + ".tmp", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, "w") as f: + f.write(secret + "\n") + os.replace(SECRET_FILE + ".tmp", SECRET_FILE) + + +def stored_secret_file(path): + cp = configparser.ConfigParser() + cp.read(path) + return cp.get("tsig", "secret_file", fallback="").strip() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--config", default=os.environ.get("SECONDDNS_CONF", "/etc/seconddns.conf")) + sub = ap.add_subparsers(dest="cmd", required=True) + r = sub.add_parser("resolve") + r.add_argument("--tsig", metavar="[ALGORITHM:]KEY:SECRET", required=True) + r.add_argument("--write", action="store_true") + c = sub.add_parser("check-secret") + c.add_argument("algorithm") + c.add_argument("secret") + sub.add_parser("secret") + sub.add_parser("status") + sub.add_parser("clear") + a = ap.parse_args() + + if a.cmd == "check-secret": + why = check_tsig_secret(a.algorithm, a.secret) + if why: + sys.exit(why) + return + + if a.cmd == "secret": + f = stored_secret_file(a.config) + if not f: + sys.exit("no TSIG secret stored") + with open(f) as fh: + print(fh.read().strip()) + return + + if a.cmd == "status": + cp = configparser.ConfigParser() + cp.read(a.config) + if not cp.has_section("tsig"): + print("TSIG: off (zones are transferred without a key)") + return + t = cp["tsig"] + print(f"TSIG key: {t.get('key_name')} ({t.get('algorithm')}), id {t.get('key_id')}") + print(f"secret: {'stored' if t.get('secret_file') and os.path.exists(t.get('secret_file')) else 'not stored'}") + key, err = resolve_tsig_key(load_config(a.config), t.get("key_name")) + if err: + sys.exit(f"account: {err}") + if key["id"] != t.get("key_id"): + sys.exit(f"account: a key named {key['name']} exists with another id ({key['id']}); it was recreated — run enable again") + print(f"account: present, label '{key['label']}'") + return + + if a.cmd == "clear": + f = stored_secret_file(a.config) + write_section(a.config, "") + if f and os.path.exists(f): + os.remove(f) + return + + algorithm, ref, secret, err = parse_tsig_pair(a.tsig) + if err: + sys.exit(err) + key, err = resolve_tsig_key(load_config(a.config), ref) + if err: + sys.exit(err) + if algorithm and algorithm != key["algorithm"]: + sys.exit(f"{key['name']} is {key['algorithm']}, not {algorithm}") + why = check_tsig_secret(key["algorithm"], secret) + if why: + sys.exit(f"{key['name']}: {why}") + if a.write: + write_secret(secret) + write_section(a.config, f"key_id = {key['id']}\nkey_name = {key['name']}\nalgorithm = {key['algorithm']}\nsecret_file = {SECRET_FILE}\n") + print(key["id"], key["name"], key["algorithm"]) + + +if __name__ == "__main__": + main() diff --git a/hosting-panels/common/seconddns_common.py b/hosting-panels/common/seconddns_common.py index 868c3c9..7f34de1 100644 --- a/hosting-panels/common/seconddns_common.py +++ b/hosting-panels/common/seconddns_common.py @@ -2,6 +2,8 @@ # Licensed under GNU General Public License v3.0 or SecondDNS Commercial License # See LICENSE (GPLv3) or LICENSE.COMMERCIAL (commercial) for details """Shared bits for the SecondDNS command-line tools (config, API, panel domains).""" +import base64 +import binascii import configparser import json import os @@ -212,3 +214,47 @@ def enqueue(op, domain, master_ip=""): """Hand an operation to the local queue. Returns True when accepted.""" r = subprocess.run([QUEUE_BIN, "enqueue", op, domain, master_ip], capture_output=True, text=True) return r.returncode == 0 + + +# Secret length per algorithm; keys created in the dashboard use exactly these. +TSIG_SECRET_BYTES = {"hmac-sha256": 32, "hmac-sha512": 64} + + +def resolve_tsig_key(cfg, ref): + """The account's TSIG key by label or full name, as shown in Settings › DNS. Returns (key, error).""" + code, keys = api(cfg, "GET", "/api/user/tsig-keys", ua="SecondDNS-Tsig/1.0") + if code != 200 or not isinstance(keys, list): + return None, f"GET /api/user/tsig-keys failed: HTTP {code}" + key = next((k for k in keys if ref and ref in (k.get("label"), k.get("name"))), None) + if key: + return key, None + have = ", ".join(f"{k.get('name')} ({k.get('label')})" for k in keys) or "none — create one in Settings › DNS" + return None, f"no TSIG key '{ref}' in this account; keys: {have}" + + +def parse_tsig_pair(value): + """'[algorithm:]key:secret', as dig -y takes it. Returns (algorithm or None, key, secret, error).""" + parts = (value or "").split(":") + if len(parts) == 2: + algorithm, (ref, secret) = None, parts + elif len(parts) == 3: + algorithm, ref, secret = parts + else: + return None, None, None, "expected --tsig=KEY:SECRET (or ALGORITHM:KEY:SECRET)" + if not ref or not secret: + return None, None, None, "both the key and the secret are needed: --tsig=KEY:SECRET" + return algorithm, ref, secret, None + + +def check_tsig_secret(algorithm, secret): + """None when the secret fits the algorithm, else the reason.""" + want = TSIG_SECRET_BYTES.get(algorithm) + if not want: + return f"unsupported algorithm {algorithm}" + try: + raw = base64.b64decode(secret, validate=True) + except (binascii.Error, ValueError): + return "the secret is not valid base64" + if len(raw) != want: + return f"the secret is {len(raw)} bytes, {algorithm} keys are {want}" + return None diff --git a/hosting-panels/common/tests/test_tsig.sh b/hosting-panels/common/tests/test_tsig.sh new file mode 100755 index 0000000..46d7cfe --- /dev/null +++ b/hosting-panels/common/tests/test_tsig.sh @@ -0,0 +1,146 @@ +#!/bin/bash +# Copyright © 2025-2026 SecondDNS +# Licensed under GNU General Public License v3.0 or SecondDNS Commercial License +# See LICENSE (GPLv3) or LICENSE.COMMERCIAL (commercial) for details +# seconddns-tsig (key lookup, secret check, [tsig] section, secret file) and +# the queue sending tsigKeyId. Requires: bash, python3, sqlite3. No network. +# +# Usage: ./test_tsig.sh + +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +TSIG="$HERE/../seconddns-tsig" +QUEUE="$HERE/../seconddns-queue" +export SECONDDNS_QUEUED_BIN="$HERE/../seconddns-queued" +TMP="$(mktemp -d)" +PORT=8897 + +export SECONDDNS_CONF="$TMP/seconddns.conf" +export SECONDDNS_TSIG_SECRET="$TMP/etc-seconddns/tsig.secret" +export SECONDDNS_QUEUE_DB="$TMP/queue.db" +export SECONDDNS_QUEUE_LOCK="$TMP/queue.lock" +export SECONDDNS_LOG="$TMP/seconddns.log" +export SECONDDNS_QUEUE_SOCK="$TMP/w.sock" + +write_conf() { + cat > "$SECONDDNS_CONF" < /dev/null 2>&1 <<'PYEOF' & +import http.server, sys +port, tmp = int(sys.argv[1]), sys.argv[2] +class H(http.server.BaseHTTPRequestHandler): + def log_message(self, *a): pass + def reply(self, code, body=b'{}'): + self.send_response(code); self.send_header('Content-Type','application/json'); self.end_headers(); self.wfile.write(body) + def do_GET(self): + if self.path.split('?')[0] == '/api/user/tsig-keys': + self.reply(200, open(tmp+'/keys','rb').read()); return + self.reply(404) + def do_POST(self): + body = self.rfile.read(int(self.headers.get('Content-Length', 0))) + with open(tmp+'/posts.log','ab') as f: f.write(body + b'\n') + try: mode = open(tmp+'/mode').read().strip() + except Exception: mode = 'ok' + self.reply(422, b'{"error":"unknown tsigKeyId"}') if mode == 'reject' else self.reply(201, b'{"id":"z1"}') +http.server.HTTPServer(('127.0.0.1', port), H).serve_forever() +PYEOF +MOCK_PID=$! +disown $MOCK_PID 2>/dev/null || true +trap 'kill $MOCK_PID 2>/dev/null; rm -rf "$TMP"' EXIT +sleep 1 + +KEYS_TWO='[{"id":"k1","label":"main","name":"acct-main","algorithm":"hmac-sha256","isDefault":false}, + {"id":"k2","label":"web","name":"acct-web","algorithm":"hmac-sha512","isDefault":true}]' +SECRET256=$(python3 -c 'import base64,os;print(base64.b64encode(os.urandom(32)).decode())') +SECRET512=$(python3 -c 'import base64,os;print(base64.b64encode(os.urandom(64)).decode())') +write_conf + +echo "== 1. resolve: one KEY:SECRET pair, key by label or full name" +echo "$KEYS_TWO" > "$TMP/keys" +"$TSIG" resolve > /dev/null 2>&1 && fail "resolve without --tsig accepted" || ok "resolve without --tsig refused" +"$TSIG" resolve --tsig=main > /dev/null 2>&1 && fail "key without a secret accepted" || ok "key without a secret refused" +assert_eq "$("$TSIG" resolve --tsig="main:$SECRET256")" "k1 acct-main hmac-sha256" "label picks that key" +assert_eq "$("$TSIG" resolve --tsig="acct-web:$SECRET512")" "k2 acct-web hmac-sha512" "full name picks that key" +assert_eq "$("$TSIG" resolve --tsig="hmac-sha256:acct-main:$SECRET256")" "k1 acct-main hmac-sha256" "dig -y form with the algorithm" +"$TSIG" resolve --tsig="hmac-sha512:main:$SECRET256" > /dev/null 2>&1 && fail "wrong algorithm prefix accepted" || ok "algorithm prefix must match the key" +"$TSIG" resolve --tsig="web:$SECRET256" > /dev/null 2>&1 && fail "secret of the wrong length accepted" || ok "secret checked against the key's algorithm" +err=$("$TSIG" resolve --tsig="nope:$SECRET256" 2>&1 >/dev/null); rc=$? +[ "$rc" -ne 0 ] && ok "unknown key exits non-zero" || fail "unknown key accepted" +echo "$err" | grep -q "acct-main (main), acct-web (web)" && ok "the error lists the account's keys" || fail "the error does not list the keys: $err" + +echo "== 2. check-secret" +"$TSIG" check-secret hmac-sha256 "$SECRET256" && ok "32-byte secret fits hmac-sha256" || fail "32-byte secret rejected" +"$TSIG" check-secret hmac-sha512 "$SECRET256" 2>/dev/null && fail "32-byte secret accepted for hmac-sha512" || ok "32-byte secret rejected for hmac-sha512" +"$TSIG" check-secret hmac-sha512 "$SECRET512" && ok "64-byte secret fits hmac-sha512" || fail "64-byte secret rejected" +"$TSIG" check-secret hmac-sha256 'not*base64' 2>/dev/null && fail "non-base64 accepted" || ok "non-base64 rejected" +"$TSIG" check-secret hmac-md5 "$SECRET256" 2>/dev/null && fail "hmac-md5 accepted" || ok "unsupported algorithm rejected" + +echo "== 3. resolve --write: section and secret file" +echo "$KEYS_TWO" > "$TMP/keys" +"$TSIG" resolve --tsig="main:$SECRET512" --write > /dev/null 2>&1 && fail "secret of the wrong length written" || ok "wrong-length secret refused before writing" +[ -e "$SECONDDNS_TSIG_SECRET" ] && fail "secret file created by a refused write" || ok "nothing written on refusal" +"$TSIG" resolve --tsig="main:$SECRET256" --write > /dev/null +section=$(python3 -c "import configparser,sys;c=configparser.ConfigParser();c.read(sys.argv[1]);print(c['tsig']['key_id'],c['tsig']['key_name'],c['tsig']['algorithm'],c['tsig']['secret_file'])" "$SECONDDNS_CONF") +assert_eq "$section" "k1 acct-main hmac-sha256 $SECONDDNS_TSIG_SECRET" "[tsig] section written" +grep -q "$SECRET256" "$SECONDDNS_CONF" && fail "secret leaked into the world-readable config" || ok "secret not in the config" +assert_eq "$(stat -c %a "$SECONDDNS_TSIG_SECRET" 2>/dev/null || stat -f %Lp "$SECONDDNS_TSIG_SECRET")" 600 "secret file is 0600" +assert_eq "$("$TSIG" secret)" "$SECRET256" "secret reads back" +grep -q '^api_key = test-key$' "$SECONDDNS_CONF" && ok "rest of the config kept" || fail "rest of the config lost" +assert_eq "$(stat -c %a "$SECONDDNS_CONF" 2>/dev/null || stat -f %Lp "$SECONDDNS_CONF")" 644 "config mode kept" +"$TSIG" resolve --tsig="web:$SECRET512" --write > /dev/null +assert_eq "$(grep -c '^\[tsig\]' "$SECONDDNS_CONF")" 1 "rewrite keeps one [tsig] section" +assert_eq "$(python3 -c "import configparser,sys;c=configparser.ConfigParser();c.read(sys.argv[1]);print(c['tsig']['key_id'])" "$SECONDDNS_CONF")" k2 "rewrite replaces the key" + +echo "== 4. status" +"$TSIG" resolve --tsig="main:$SECRET256" --write > /dev/null +out=$("$TSIG" status); rc=$? +assert_eq "$rc" 0 "status exits 0 while the key is in the account" +echo "$out" | grep -q "TSIG key: acct-main (hmac-sha256), id k1" && ok "status names the key" || fail "status output: $out" +echo "$out" | grep -q "label 'main'" && ok "status shows the label" || fail "status label: $out" +echo '[{"id":"k9","label":"main","name":"acct-main","algorithm":"hmac-sha256","isDefault":false}]' > "$TMP/keys" +"$TSIG" status > /dev/null 2>&1 && fail "recreated key reported as fine" || ok "recreated key (new id) exits non-zero" +echo '[]' > "$TMP/keys" +"$TSIG" status > /dev/null 2>&1 && fail "deleted key reported as fine" || ok "deleted key exits non-zero" +echo "$KEYS_TWO" > "$TMP/keys" + +echo "== 5. queue sends tsigKeyId only with a [tsig] section" +"$TSIG" resolve --tsig="main:$SECRET256" --write > /dev/null +: > "$TMP/posts.log" +"$QUEUE" enqueue create signed.example.com 192.0.2.10 && "$QUEUE" flush +assert_eq "$(python3 -c "import json,sys;print(json.loads(open(sys.argv[1]).readline()).get('tsigKeyId'))" "$TMP/posts.log")" k1 "create carries tsigKeyId" +echo reject > "$TMP/mode" +"$QUEUE" enqueue create rejected.example.com 192.0.2.10; "$QUEUE" flush +grep -q "rejected with TSIG key k1" "$SECONDDNS_LOG" && ok "422 with a key is logged with the key id" || fail "422 with a key not explained in the log" +rm -f "$TMP/mode" +"$TSIG" clear +grep -q '^\[tsig\]' "$SECONDDNS_CONF" && fail "clear left the section" || ok "clear removes the section" +assert_eq "$("$TSIG" status)" "TSIG: off (zones are transferred without a key)" "status after clear" +[ -e "$SECONDDNS_TSIG_SECRET" ] && fail "clear left the secret file" || ok "clear removes the secret file" +: > "$TMP/posts.log" +"$QUEUE" enqueue create plain.example.com 192.0.2.10 && "$QUEUE" flush +assert_eq "$(head -1 "$TMP/posts.log")" '{"name": "plain.example.com", "masterIp": "192.0.2.10"}' "without [tsig] the create body is unchanged" + +echo +echo "passed: $PASS, failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/cpanel/install.sh b/hosting-panels/cpanel/install.sh index 28682f7..a43e187 100755 --- a/hosting-panels/cpanel/install.sh +++ b/hosting-panels/cpanel/install.sh @@ -272,6 +272,7 @@ curl -sf --max-time 10 -o /usr/local/bin/seconddns-queue "$COMMON_URL/seconddns- curl -sf --max-time 10 -o /usr/local/bin/seconddns-queued "$COMMON_URL/seconddns-queued?t=$(date +%s)" curl -sf --max-time 10 -o /etc/systemd/system/seconddns-queued.service "$COMMON_URL/seconddns-queued.service?t=$(date +%s)" chmod +x /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +curl -sf --max-time 10 -o /usr/local/bin/seconddns-tsig "$COMMON_URL/seconddns-tsig?t=$(date +%s)" && chmod +x /usr/local/bin/seconddns-tsig bash <(curl -sf --max-time 10 "$COMMON_URL/install-idn2.sh?t=$(date +%s)") mkdir -p /var/lib/seconddns bash <(curl -sf --max-time 10 "$COMMON_URL/install-sqlite.sh?t=$(date +%s)") diff --git a/hosting-panels/cpanel/uninstall.sh b/hosting-panels/cpanel/uninstall.sh index cb3c6f4..482d471 100755 --- a/hosting-panels/cpanel/uninstall.sh +++ b/hosting-panels/cpanel/uninstall.sh @@ -106,6 +106,8 @@ systemctl disable --now seconddns-queued.service 2>/dev/null rm -f /etc/systemd/system/seconddns-queued.service systemctl daemon-reload 2>/dev/null rm -f /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns_common.py /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +rm -f /usr/local/bin/seconddns-tsig /etc/seconddns/tsig.secret +rmdir /etc/seconddns 2>/dev/null || true if [ -f /var/lib/seconddns/queue.db ] && command -v sqlite3 &>/dev/null; then n=$(sqlite3 /var/lib/seconddns/queue.db "SELECT COUNT(*) FROM ops WHERE status='pending';" 2>/dev/null || echo 0) [ "${n:-0}" -gt 0 ] && echo "[!] Discarding $n pending zone operation(s) that were never delivered to SecondDNS" diff --git a/hosting-panels/cyberpanel/install.sh b/hosting-panels/cyberpanel/install.sh index 7ebb7e8..b0f3e95 100755 --- a/hosting-panels/cyberpanel/install.sh +++ b/hosting-panels/cyberpanel/install.sh @@ -281,6 +281,7 @@ cp "$COMMON_SRC/seconddns-queue" /usr/local/bin/seconddns-queue cp "$COMMON_SRC/seconddns-queued" /usr/local/bin/seconddns-queued cp "$COMMON_SRC/seconddns-queued.service" /etc/systemd/system/seconddns-queued.service chmod +x /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +cp "$COMMON_SRC/seconddns-tsig" /usr/local/bin/seconddns-tsig && chmod +x /usr/local/bin/seconddns-tsig bash "$COMMON_SRC/install-idn2.sh" mkdir -p /var/lib/seconddns bash "$COMMON_SRC/install-sqlite.sh" diff --git a/hosting-panels/cyberpanel/uninstall.sh b/hosting-panels/cyberpanel/uninstall.sh index 4c2e93d..ca24855 100755 --- a/hosting-panels/cyberpanel/uninstall.sh +++ b/hosting-panels/cyberpanel/uninstall.sh @@ -93,6 +93,8 @@ systemctl disable --now seconddns-queued.service 2>/dev/null rm -f /etc/systemd/system/seconddns-queued.service systemctl daemon-reload 2>/dev/null rm -f /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns_common.py /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +rm -f /usr/local/bin/seconddns-tsig /etc/seconddns/tsig.secret +rmdir /etc/seconddns 2>/dev/null || true if [ -f /var/lib/seconddns/queue.db ] && command -v sqlite3 &>/dev/null; then n=$(sqlite3 /var/lib/seconddns/queue.db "SELECT COUNT(*) FROM ops WHERE status='pending';" 2>/dev/null || echo 0) [ "${n:-0}" -gt 0 ] && echo "[!] Discarding $n pending zone operation(s) that were never delivered to SecondDNS" diff --git a/hosting-panels/directadmin/install.sh b/hosting-panels/directadmin/install.sh index e95c9f4..59b5d75 100644 --- a/hosting-panels/directadmin/install.sh +++ b/hosting-panels/directadmin/install.sh @@ -269,6 +269,7 @@ curl -sf --max-time 10 -o /usr/local/bin/seconddns-queue "$COMMON_URL/seconddns- curl -sf --max-time 10 -o /usr/local/bin/seconddns-queued "$COMMON_URL/seconddns-queued?t=$(date +%s)" curl -sf --max-time 10 -o /etc/systemd/system/seconddns-queued.service "$COMMON_URL/seconddns-queued.service?t=$(date +%s)" chmod +x /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +curl -sf --max-time 10 -o /usr/local/bin/seconddns-tsig "$COMMON_URL/seconddns-tsig?t=$(date +%s)" && chmod +x /usr/local/bin/seconddns-tsig bash <(curl -sf --max-time 10 "$COMMON_URL/install-idn2.sh?t=$(date +%s)") mkdir -p /var/lib/seconddns bash <(curl -sf --max-time 10 "$COMMON_URL/install-sqlite.sh?t=$(date +%s)") diff --git a/hosting-panels/directadmin/uninstall.sh b/hosting-panels/directadmin/uninstall.sh index 0d00002..0e5f9e3 100644 --- a/hosting-panels/directadmin/uninstall.sh +++ b/hosting-panels/directadmin/uninstall.sh @@ -37,6 +37,8 @@ systemctl disable --now seconddns-queued.service 2>/dev/null rm -f /etc/systemd/system/seconddns-queued.service systemctl daemon-reload 2>/dev/null rm -f /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns_common.py /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +rm -f /usr/local/bin/seconddns-tsig /etc/seconddns/tsig.secret +rmdir /etc/seconddns 2>/dev/null || true if [ -f /var/lib/seconddns/queue.db ] && command -v sqlite3 &>/dev/null; then n=$(sqlite3 /var/lib/seconddns/queue.db "SELECT COUNT(*) FROM ops WHERE status='pending';" 2>/dev/null || echo 0) [ "${n:-0}" -gt 0 ] && echo "[!] Discarding $n pending zone operation(s) that were never delivered to SecondDNS" diff --git a/hosting-panels/plesk/install.sh b/hosting-panels/plesk/install.sh index e8b9626..249492c 100755 --- a/hosting-panels/plesk/install.sh +++ b/hosting-panels/plesk/install.sh @@ -275,6 +275,7 @@ curl -sf --max-time 10 -o /usr/local/bin/seconddns-queue "$COMMON_URL/seconddns- curl -sf --max-time 10 -o /usr/local/bin/seconddns-queued "$COMMON_URL/seconddns-queued?t=$(date +%s)" curl -sf --max-time 10 -o /etc/systemd/system/seconddns-queued.service "$COMMON_URL/seconddns-queued.service?t=$(date +%s)" chmod +x /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +curl -sf --max-time 10 -o /usr/local/bin/seconddns-tsig "$COMMON_URL/seconddns-tsig?t=$(date +%s)" && chmod +x /usr/local/bin/seconddns-tsig bash <(curl -sf --max-time 10 "$COMMON_URL/install-idn2.sh?t=$(date +%s)") mkdir -p /var/lib/seconddns bash <(curl -sf --max-time 10 "$COMMON_URL/install-sqlite.sh?t=$(date +%s)") diff --git a/hosting-panels/plesk/uninstall.sh b/hosting-panels/plesk/uninstall.sh index 8b7bb71..7b4b97d 100755 --- a/hosting-panels/plesk/uninstall.sh +++ b/hosting-panels/plesk/uninstall.sh @@ -127,6 +127,8 @@ systemctl disable --now seconddns-queued.service 2>/dev/null rm -f /etc/systemd/system/seconddns-queued.service systemctl daemon-reload 2>/dev/null rm -f /usr/local/bin/seconddns /usr/local/bin/seconddns-domain /usr/local/bin/seconddns-owner /usr/local/bin/seconddns-migrate-master /usr/local/bin/seconddns-reconcile /usr/local/bin/seconddns_common.py /usr/local/bin/seconddns-queue /usr/local/bin/seconddns-queued +rm -f /usr/local/bin/seconddns-tsig /etc/seconddns/tsig.secret +rmdir /etc/seconddns 2>/dev/null || true if [ -f /var/lib/seconddns/queue.db ] && command -v sqlite3 &>/dev/null; then n=$(sqlite3 /var/lib/seconddns/queue.db "SELECT COUNT(*) FROM ops WHERE status='pending';" 2>/dev/null || echo 0) [ "${n:-0}" -gt 0 ] && echo "[!] Discarding $n pending zone operation(s) that were never delivered to SecondDNS"