From f3232dba0a5bf57b836bd8b0ae922f4b04dc67bb Mon Sep 17 00:00:00 2001 From: DTZSGHNR <2753086015@qq.com> Date: Sun, 26 Apr 2026 00:01:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(alidnsddns):=20=E6=96=B0=E5=A2=9E=E9=98=BF?= =?UTF-8?q?=E9=87=8C=E4=BA=91=20DDNS=20=E6=8F=92=E4=BB=B6=20v1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定时检测公网 IPv4/IPv6 地址,自动更新阿里云 DNS 解析记录 - 支持泛域名(* 记录)、根域(@ 记录)及任意子域名 - 支持同时维护多条 A / AAAA 记录 - 详情页展示更新历史(VDataTable,最多 100 条) - IP 变化时推送通知(兼容所有通知渠道) - 纯标准库实现阿里云 DNS API(HMAC-SHA1 签名),无额外依赖 --- icons/AliDnsDDNS.png | Bin 0 -> 46307 bytes package.json | 12 + plugins/alidnsddns/__init__.py | 619 +++++++++++++++++++++++++++++++++ 3 files changed, 631 insertions(+) create mode 100644 icons/AliDnsDDNS.png create mode 100644 plugins/alidnsddns/__init__.py diff --git a/icons/AliDnsDDNS.png b/icons/AliDnsDDNS.png new file mode 100644 index 0000000000000000000000000000000000000000..2745efb8b29a3b8ba7abf088a825766db1d52859 GIT binary patch literal 46307 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 H3AprMV$5yh literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 968b6ef..8af8f18 100644 --- a/package.json +++ b/package.json @@ -1061,5 +1061,17 @@ "author": "cddjr", "level": 1, "v2": true + }, + "AliDnsDDNS": { + "name": "阿里云 DDNS", + "description": "定时检测公网 IP,自动更新阿里云 DNS 解析记录,支持泛域名(* 记录)及 IPv6(AAAA)。", + "labels": "网络", + "version": "1.0", + "icon": "AliDnsDDNS.png", + "author": "dtzsghnr", + "level": 1, + "history": { + "v1.0": "初始版本,支持 IPv4/IPv6、泛域名、多记录配置、更新历史详情页" + } } } \ No newline at end of file diff --git a/plugins/alidnsddns/__init__.py b/plugins/alidnsddns/__init__.py new file mode 100644 index 0000000..7084674 --- /dev/null +++ b/plugins/alidnsddns/__init__.py @@ -0,0 +1,619 @@ +import hashlib +import hmac +import random +import socket +import string +import urllib.parse +import urllib.request +from base64 import b64encode +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Tuple + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.interval import IntervalTrigger + +from app.core.config import settings +from app.log import logger +from app.plugins import _PluginBase +from app.schemas.types import NotificationType + +_MAX_HISTORY = 100 + + +class AliDnsDDNS(_PluginBase): + # ────────────────────────────────────────────── + # 插件元数据 + # ────────────────────────────────────────────── + plugin_name = "阿里云 DDNS" + plugin_desc = "定时检测公网 IP,自动更新阿里云 DNS 解析记录,支持泛域名(* 记录)及 IPv6(AAAA)。" + plugin_icon = "AliDnsDDNS.png" + plugin_version = "1.0" + plugin_author = "dtzsghnr" + author_url = "https://github.com/dtzsghnr" + plugin_config_prefix = "alidnsddns_" + plugin_order = 30 + auth_level = 1 + + # ────────────────────────────────────────────── + # 私有状态 + # ────────────────────────────────────────────── + _enabled: bool = False + _access_key_id: str = "" + _access_key_secret: str = "" + _records: str = "" + _interval: int = 5 + _notify: bool = True + _run_once: bool = False + + _scheduler: Optional[BackgroundScheduler] = None + _last_ipv4: str = "" + _last_ipv6: str = "" + + # ────────────────────────────────────────────── + # 生命周期 + # ────────────────────────────────────────────── + + def init_plugin(self, config: dict = None): + if config: + self._enabled = config.get("enabled", False) + self._access_key_id = config.get("access_key_id", "").strip() + self._access_key_secret = config.get("access_key_secret", "").strip() + self._records = config.get("records", "").strip() + self._interval = max(1, int(config.get("interval", 5) or 5)) + self._notify = config.get("notify", True) + self._run_once = config.get("run_once", False) + + self.stop_service() + + if not self._enabled: + return + + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + if self._run_once: + self._scheduler.add_job( + func=self.__update_dns, + trigger="date", + run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3), + name="阿里云DDNS 立即执行", + ) + self._run_once = False + self.update_config({ + "enabled": self._enabled, + "access_key_id": self._access_key_id, + "access_key_secret": self._access_key_secret, + "records": self._records, + "interval": self._interval, + "notify": self._notify, + "run_once": False, + }) + + self._scheduler.add_job( + func=self.__update_dns, + trigger=IntervalTrigger(minutes=self._interval), + name="阿里云DDNS 定时任务", + ) + + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + record_count = len(_parse_records(self._records)) + logger.info( + f"[AliDnsDDNS] 插件已启动 | 检测间隔: {self._interval}min | " + f"记录数: {record_count}" + ) + + def get_state(self) -> bool: + return self._enabled + + def stop_service(self): + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error(f"[AliDnsDDNS] 停止调度器失败: {e}") + + # ────────────────────────────────────────────── + # 服务注册(宿主调度器) + # ────────────────────────────────────────────── + + def get_service(self) -> List[Dict[str, Any]]: + if self._enabled and self._interval: + return [{ + "id": "AliDnsDDNS", + "name": "阿里云 DDNS 更新", + "trigger": IntervalTrigger(minutes=self._interval), + "func": self.__update_dns, + "kwargs": {}, + }] + return [] + + def get_command(self) -> List[Dict[str, Any]]: + return [] + + def get_api(self) -> List[Dict[str, Any]]: + return [ + { + "path": "/alidnsddns/history", + "endpoint": self.__api_history, + "methods": ["GET"], + "summary": "获取 DDNS 更新历史", + }, + { + "path": "/alidnsddns/history/clear", + "endpoint": self.__api_clear_history, + "methods": ["POST"], + "summary": "清空 DDNS 更新历史", + }, + ] + + # ────────────────────────────────────────────── + # API + # ────────────────────────────────────────────── + + def __api_history(self) -> List[dict]: + return self.get_data("history") or [] + + def __api_clear_history(self) -> dict: + self.del_data("history") + logger.info("[AliDnsDDNS] 更新历史已清空") + return {"success": True} + + # ────────────────────────────────────────────── + # 配置表单 + # ────────────────────────────────────────────── + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + return [ + { + "component": "VForm", + "content": [ + # ── 开关行 ── + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [{ + "component": "VSwitch", + "props": {"model": "enabled", "label": "启用插件"}, + }], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [{ + "component": "VSwitch", + "props": {"model": "notify", "label": "IP 变化时发送通知"}, + }], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [{ + "component": "VSwitch", + "props": {"model": "run_once", "label": "立即运行一次"}, + }], + }, + ], + }, + # ── 密钥行 ── + { + "component": "VRow", + "content": [ + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [{ + "component": "VTextField", + "props": { + "model": "access_key_id", + "label": "AccessKey ID", + "placeholder": "LTAI5t...", + "hint": "阿里云 RAM 访问密钥 ID", + }, + }], + }, + { + "component": "VCol", + "props": {"cols": 12, "md": 6}, + "content": [{ + "component": "VTextField", + "props": { + "model": "access_key_secret", + "label": "AccessKey Secret", + "placeholder": "xxxx...", + "type": "password", + "hint": "阿里云 RAM 访问密钥 Secret", + }, + }], + }, + ], + }, + # ── 间隔 ── + { + "component": "VRow", + "content": [{ + "component": "VCol", + "props": {"cols": 12, "md": 4}, + "content": [{ + "component": "VTextField", + "props": { + "model": "interval", + "label": "检测间隔(分钟)", + "type": "number", + "placeholder": "5", + "hint": "最小 1 分钟", + }, + }], + }], + }, + # ── 记录列表 ── + { + "component": "VRow", + "content": [{ + "component": "VCol", + "props": {"cols": 12}, + "content": [{ + "component": "VTextarea", + "props": { + "model": "records", + "label": "DNS 记录列表", + "rows": 6, + "placeholder": ( + "格式:顶级域名 主机记录 类型(类型可省略默认A)\n" + "example.com @ A # example.com 根域 IPv4\n" + "example.com * A # *.example.com 泛域名 IPv4\n" + "example.com home A # home.example.com IPv4\n" + "example.com home AAAA # home.example.com IPv6" + ), + "hint": "第一列:阿里云注册的顶级域(如 example.com);第二列:主机记录前缀(@ 根域 / * 泛域名 / 子域名前缀)", + "persistent-hint": True, + }, + }], + }], + }, + # ── 说明 ── + { + "component": "VRow", + "content": [{ + "component": "VCol", + "props": {"cols": 12}, + "content": [{ + "component": "VAlert", + "props": { + "type": "info", + "variant": "tonal", + "text": ( + "需要在阿里云 RAM 控制台为 AccessKey 授予 AliyunDNSFullAccess 权限。" + "第一列必须是阿里云中托管的顶级域(如 example.com),第二列是主机记录前缀。" + "更新 home.example.com 填:example.com home A。" + "泛域名填 *,根域填 @,IPv6 类型填 AAAA。" + ), + }, + }], + }], + }, + ], + } + ], { + "enabled": False, + "access_key_id": "", + "access_key_secret": "", + "records": "", + "interval": 5, + "notify": True, + "run_once": False, + } + + # ────────────────────────────────────────────── + # 详情页 + # ────────────────────────────────────────────── + + def get_page(self) -> List[dict]: + history: List[dict] = self.get_data("history") or [] + + if not history: + return [{ + "component": "div", + "props": {"class": "text-center pa-6 text-medium-emphasis"}, + "text": "暂无更新记录", + }] + + history = sorted(history, key=lambda x: x.get("update_time", ""), reverse=True) + + return [{ + "component": "VDataTable", + "props": { + "headers": [ + {"title": "域名", "key": "fqdn", "sortable": True}, + {"title": "类型", "key": "type", "sortable": True, "width": "80px"}, + {"title": "IP 地址", "key": "ip", "sortable": False}, + {"title": "更新时间", "key": "update_time", "sortable": True}, + ], + "items": history, + "density": "comfortable", + "hover": True, + "items-per-page": 20, + }, + }] + + # ────────────────────────────────────────────── + # 核心逻辑 + # ────────────────────────────────────────────── + + def __update_dns(self): + if not self._access_key_id or not self._access_key_secret: + logger.warning("[AliDnsDDNS] AccessKey 未配置,跳过本次检测") + return + + parsed = _parse_records(self._records) + if not parsed: + logger.warning("[AliDnsDDNS] 记录列表为空,跳过本次检测") + return + + need_v4 = any(r["type"] == "A" for r in parsed) + need_v6 = any(r["type"] == "AAAA" for r in parsed) + + ipv4 = _get_public_ip(v6=False) if need_v4 else None + ipv6 = _get_public_ip(v6=True) if need_v6 else None + + if need_v4 and not ipv4: + logger.error("[AliDnsDDNS] 公网 IPv4 获取失败,跳过本次更新") + return + if need_v6 and not ipv6: + logger.error("[AliDnsDDNS] 公网 IPv6 获取失败,跳过本次更新") + return + + client = _AliDnsClient(self._access_key_id, self._access_key_secret) + updated: List[dict] = [] + now_str = datetime.now(tz=pytz.timezone(settings.TZ)).strftime("%Y-%m-%d %H:%M:%S") + + for rec in parsed: + ip = ipv4 if rec["type"] == "A" else ipv6 + fqdn = _fqdn(rec["rr"], rec["domain"]) + try: + changed = client.upsert(rec["domain"], rec["rr"], rec["type"], ip) + if changed: + updated.append({"fqdn": fqdn, "type": rec["type"], + "ip": ip, "update_time": now_str}) + logger.info( + f"[AliDnsDDNS] 记录已更新 | {fqdn} | {rec['type']} | {ip}" + ) + else: + logger.debug( + f"[AliDnsDDNS] 记录无变化 | {fqdn} | {rec['type']} | {ip}" + ) + except Exception as e: + logger.error( + f"[AliDnsDDNS] 记录更新失败 | {fqdn} | {rec['type']} | 原因: {e}" + ) + + if ipv4: + self._last_ipv4 = ipv4 + if ipv6: + self._last_ipv6 = ipv6 + + if not updated: + return + + self.__save_history(updated) + + if self._notify: + self.__send_notify(updated) + + def __save_history(self, new_items: List[dict]): + history: List[dict] = self.get_data("history") or [] + history = (new_items + history)[:_MAX_HISTORY] + self.save_data("history", history) + + def __send_notify(self, updated: List[dict]): + blocks = [] + for item in updated: + type_label = "IPv4" if item["type"] == "A" else "IPv6" + blocks.append(f"{item['fqdn']}({type_label})\n{item['ip']}") + + text = "以下记录已同步新 IP:\n\n" + "\n\n".join(blocks) + "\n\n查看详情" + + self.post_message( + mtype=NotificationType.Plugin, + title="🌐 阿里云 DDNS 已更新", + text=text, + ) + + +# ────────────────────────────────────────────────────────────────────────────── +# 工具函数 +# ────────────────────────────────────────────────────────────────────────────── + +def _fqdn(rr: str, domain: str) -> str: + return domain if rr == "@" else f"{rr}.{domain}" + + +def _parse_records(raw: str) -> List[Dict[str, str]]: + """ + 格式:顶级域名 主机记录 [类型] + 示例: + example.com @ A + example.com * A + example.com home AAAA + 空行和 # 注释行会被跳过。 + """ + result = [] + for line in raw.splitlines(): + line = line.split("#")[0].strip() + if not line: + continue + parts = line.split() + if len(parts) < 2: + logger.warning(f"[AliDnsDDNS] 忽略无效配置行: {line!r}") + continue + domain = parts[0] + rr = parts[1] + rec_type = parts[2].upper() if len(parts) >= 3 else "A" + if rec_type not in ("A", "AAAA"): + logger.warning( + f"[AliDnsDDNS] 不支持的记录类型 {rec_type!r},已跳过: {line!r}" + ) + continue + result.append({"domain": domain, "rr": rr, "type": rec_type}) + return result + + +def _get_public_ip(v6: bool = False) -> Optional[str]: + """从多个公共端点轮询获取公网 IPv4 或 IPv6 地址,首个成功即返回。""" + sources_v4 = [ + "https://api4.ipify.org", + "https://ipv4.icanhazip.com", + "https://myexternalip.com/raw", + "https://ipecho.net/plain", + ] + sources_v6 = [ + "https://api6.ipify.org", + "https://ipv6.icanhazip.com", + "https://6.ident.me", + ] + sources = sources_v6 if v6 else sources_v4 + validate = _is_valid_ipv6 if v6 else _is_valid_ipv4 + label = "IPv6" if v6 else "IPv4" + + for url in sources: + try: + req = urllib.request.Request( + url, headers={"User-Agent": "MoviePilot-AliDnsDDNS/1.0"} + ) + with urllib.request.urlopen(req, timeout=8) as resp: + ip = resp.read(64).decode().strip() + if validate(ip): + logger.debug(f"[AliDnsDDNS] 检测到公网 {label}: {ip}(来源: {url})") + return ip + logger.debug(f"[AliDnsDDNS] {url} 返回无效 {label}: {ip!r}") + except Exception as e: + logger.debug(f"[AliDnsDDNS] {label} 检测源不可用: {url} — {e}") + + logger.warning(f"[AliDnsDDNS] 所有 {label} 检测源均不可用") + return None + + +def _is_valid_ipv4(ip: str) -> bool: + try: + socket.inet_pton(socket.AF_INET, ip) + return True + except (OSError, socket.error): + return False + + +def _is_valid_ipv6(ip: str) -> bool: + try: + socket.inet_pton(socket.AF_INET6, ip) + return True + except (OSError, socket.error): + return False + + +# ────────────────────────────────────────────────────────────────────────────── +# 阿里云 DNS API 客户端(纯标准库) +# ────────────────────────────────────────────────────────────────────────────── + +_ALIDNS_ENDPOINT = "https://alidns.aliyuncs.com/" + + +class _AliDnsClient: + + def __init__(self, key_id: str, key_secret: str): + self._key_id = key_id + self._key_secret = key_secret + + # ── 签名 ───────────────────────────────────── + + @staticmethod + def _percent_encode(s: str) -> str: + e = urllib.parse.quote(s, safe="") + return e.replace("+", "%20").replace("*", "%2A").replace("%7E", "~") + + def _sign(self, params: Dict[str, str]) -> str: + canonical = "&".join( + f"{self._percent_encode(k)}={self._percent_encode(params[k])}" + for k in sorted(params) + ) + string_to_sign = "GET&%2F&" + self._percent_encode(canonical) + mac = hmac.new( + (self._key_secret + "&").encode(), + string_to_sign.encode(), + hashlib.sha1, + ) + return b64encode(mac.digest()).decode() + + def _base_params(self, action: str) -> Dict[str, str]: + nonce = "".join(random.choices(string.ascii_lowercase + string.digits, k=16)) + return { + "Action": action, + "Version": "2015-01-09", + "Format": "JSON", + "AccessKeyId": self._key_id, + "SignatureMethod": "HMAC-SHA1", + "SignatureVersion": "1.0", + "SignatureNonce": nonce, + "Timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + } + + def _request(self, params: Dict[str, str]) -> dict: + import json as _json + params["Signature"] = self._sign(params) + url = _ALIDNS_ENDPOINT + "?" + urllib.parse.urlencode(params) + req = urllib.request.Request( + url, headers={"User-Agent": "MoviePilot-AliDnsDDNS/1.0"} + ) + with urllib.request.urlopen(req, timeout=15) as resp: + body = _json.loads(resp.read().decode()) + if body.get("Code"): + raise RuntimeError(f"{body['Code']}: {body.get('Message', '')}") + return body + + # ── CRUD ───────────────────────────────────── + + def _list_records(self, domain: str, rr: str, rec_type: str) -> List[dict]: + p = self._base_params("DescribeDomainRecords") + p.update({"DomainName": domain, "RRKeyWord": rr, + "TypeKeyWord": rec_type, "PageSize": "20"}) + return self._request(p).get("DomainRecords", {}).get("Record", []) + + def _add_record(self, domain: str, rr: str, rec_type: str, value: str): + p = self._base_params("AddDomainRecord") + p.update({"DomainName": domain, "RR": rr, + "Type": rec_type, "Value": value, "TTL": "600"}) + self._request(p) + logger.info(f"[AliDnsDDNS] 新建 DNS 记录 | {_fqdn(rr, domain)} | {rec_type} | {value}") + + def _update_record(self, record_id: str, rr: str, rec_type: str, + value: str, domain: str = ""): + p = self._base_params("UpdateDomainRecord") + p.update({"RecordId": record_id, "RR": rr, + "Type": rec_type, "Value": value, "TTL": "600"}) + self._request(p) + + def upsert(self, domain: str, rr: str, rec_type: str, new_ip: str) -> bool: + """ + 创建或更新 DNS 记录。 + 返回 True 表示发生了变更,False 表示记录已是最新值。 + """ + records = self._list_records(domain, rr, rec_type) + # API RRKeyWord 是模糊匹配,需精确过滤 + matched = [r for r in records if r.get("RR") == rr and r.get("Type") == rec_type] + + if not matched: + self._add_record(domain, rr, rec_type, new_ip) + return True + + rec = matched[0] + if rec.get("Value") == new_ip: + return False # 已是最新 + + self._update_record(rec["RecordId"], rr, rec_type, new_ip, domain) + return True