From 240f7eb3fa595d0e005f3a97e9e5052a85bf6f6d Mon Sep 17 00:00:00 2001 From: wood chen Date: Sat, 22 Feb 2025 23:51:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81IP=E5=8F=AF=E7=94=A8=E6=A3=80?= =?UTF-8?q?=E6=B5=8B,=20=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE=E6=96=B9?= =?UTF-8?q?=E5=BC=8F,=20=E6=A3=80=E6=B5=8B=E6=97=B6=E9=97=B4=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 41 ---------- .gitignore | 1 + Dockerfile | 5 +- config.example.yaml | 36 +++++++++ config.py | 48 +++++------ docker-compose.yml | 4 +- main.py | 192 ++++++++++++++++++++++++++++++++++---------- readme.md | 52 ++++++------ requirements.txt | 3 +- 9 files changed, 243 insertions(+), 139 deletions(-) delete mode 100644 .env.example create mode 100644 config.example.yaml diff --git a/.env.example b/.env.example deleted file mode 100644 index 617002e..0000000 --- a/.env.example +++ /dev/null @@ -1,41 +0,0 @@ -# 腾讯云API配置 -TENCENT_SECRET_ID=your_secret_id_here -TENCENT_SECRET_KEY=your_secret_key_here - -# 日志级别 -LOG_LEVEL=INFO - -# 域名配置 - 域名1 -DOMAIN_1=example1.com -SUB_DOMAIN_1=@ # 子域名,@ 表示根域名 -REMARK_1=优选IP # 记录备注 -TTL_1=600 # TTL值(秒) -IPV4_ENABLED_1=true # 是否启用IPv4记录 -IPV6_ENABLED_1=true # 是否启用IPv6记录 -ENABLED_1=true # 是否启用此域名配置 - -# 域名配置 - 域名2(可选) -DOMAIN_2=example2.com -SUB_DOMAIN_2=www -REMARK_2=优选IP -TTL_2=600 -IPV4_ENABLED_2=true -IPV6_ENABLED_2=true -ENABLED_2=true - -# 域名配置 - 域名3(更长的缓存) -DOMAIN_3=example3.com -SUB_DOMAIN_3=* -REMARK_3=CloudFlare优选 # 记录备注 -TTL_3=1800 # 更长的TTL -IPV4_ENABLED_3=true # 启用IPv4 -IPV6_ENABLED_3=true # 启用IPv6 -ENABLED_3=true - -# 可以继续添加更多域名配置... -# 每个域名可以独立控制: -# - 是否启用IPv4 (IPV4_ENABLED_n) -# - 是否启用IPv6 (IPV6_ENABLED_n) -# - TTL值 (TTL_n) -# - 是否启用 (ENABLED_n) -# - 记录备注 (REMARK_n) \ No newline at end of file diff --git a/.gitignore b/.gitignore index b96c1db..8d85e25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /__pycache__ .env logs/ +config.yaml diff --git a/Dockerfile b/Dockerfile index 7b94e12..eccf595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ FROM python:3.11-slim ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +# 安装ping工具 +RUN apt-get update && apt-get install -y iputils-ping && rm -rf /var/lib/apt/lists/* + WORKDIR /app # 复制项目文件 @@ -18,4 +21,4 @@ RUN pip install -r requirements.txt RUN mkdir -p logs && touch .env # 运行程序 -CMD ["python", "main.py"] \ No newline at end of file +CMD ["python", "main.py"] \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..0c477b0 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,36 @@ +# 腾讯云API配置 +tencent: + secret_id: your_secret_id_here + secret_key: your_secret_key_here + +# 日志级别 +log_level: INFO + +# 更新检查间隔(分钟) +check_interval: 15 + +# 域名配置列表 +domains: + - domain: example1.com + sub_domain: "@" # 子域名,@ 表示根域名 + remark: 优选IP # 记录备注 + ttl: 600 # TTL值(秒) + ipv4_enabled: true # 是否启用IPv4记录 + ipv6_enabled: true # 是否启用IPv6记录 + enabled: true # 是否启用此域名配置 + + - domain: example2.com + sub_domain: www + remark: 优选IP + ttl: 600 + ipv4_enabled: true + ipv6_enabled: true + enabled: true + + - domain: example3.com + sub_domain: "*" # 泛解析 + remark: CloudFlare优选 + ttl: 1800 + ipv4_enabled: true + ipv6_enabled: true + enabled: true \ No newline at end of file diff --git a/config.py b/config.py index e4a8fd9..fdc2787 100644 --- a/config.py +++ b/config.py @@ -1,38 +1,32 @@ import os -from dotenv import load_dotenv +from typing import Dict, List +import yaml -# 加载环境变量 -load_dotenv() +# 加载YAML配置 +def load_config() -> Dict: + """从YAML文件加载配置""" + yaml_files = ["config.yaml", "config.example.yaml"] + for yaml_file in yaml_files: + if os.path.exists(yaml_file): + with open(yaml_file, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + return {} + +# 加载配置 +config_data = load_config() # 腾讯云API配置 -SECRET_ID = os.getenv("TENCENT_SECRET_ID") -SECRET_KEY = os.getenv("TENCENT_SECRET_KEY") +SECRET_ID = config_data.get("tencent", {}).get("secret_id") +SECRET_KEY = config_data.get("tencent", {}).get("secret_key") # API接口配置 API_URL = "https://api.vvhan.com/tool/cf_ip" # 日志级别 -LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") +LOG_LEVEL = config_data.get("log_level", "INFO") + +# 更新检查间隔(分钟) +check_interval = config_data.get("check_interval", 15) # 获取所有域名配置 -DOMAINS = [] -index = 1 -while True: - domain = os.getenv(f"DOMAIN_{index}") - if not domain: - break - - DOMAINS.append( - { - "domain": domain, - "sub_domain": os.getenv(f"SUB_DOMAIN_{index}", "@"), - "remark": os.getenv(f"REMARK_{index}", "优选IP"), - "ttl": int(os.getenv(f"TTL_{index}", "600")), - "ipv4_enabled": os.getenv(f"IPV4_ENABLED_{index}", "true").lower() - == "true", - "ipv6_enabled": os.getenv(f"IPV6_ENABLED_{index}", "true").lower() - == "true", - "enabled": os.getenv(f"ENABLED_{index}", "true").lower() == "true", - } - ) - index += 1 +DOMAINS = config_data.get("domains", []) diff --git a/docker-compose.yml b/docker-compose.yml index 9b4e892..6f3f0db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,5 +4,5 @@ services: container_name: dnspod-yxip restart: always volumes: - - ./.env:/app/.env:ro # 映射环境变量文件 - - ./logs:/app/logs # 映射日志目录 \ No newline at end of file + - ./config.yaml:/app/config.yaml:ro # 映射YAML配置文件 + - ./logs:/app/logs # 映射日志目录 \ No newline at end of file diff --git a/main.py b/main.py index d7b13eb..7564e31 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,9 @@ from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.dnspod.v20210323 import dnspod_client, models +import subprocess +import os +from datetime import datetime, timedelta # 配置日志 logger.add("logs/dnspod.log", rotation="10 MB", level=config.LOG_LEVEL) @@ -30,6 +33,10 @@ class DNSPodManager: self.current_ips = ( {} ) # 格式: {domain: {'默认': {'A': ip, 'AAAA': ip}, '移动': {...}}} + # IP可用性缓存,格式:{ip: {'available': bool, 'last_check': datetime}} + self.ip_availability_cache = {} + # IP可用性缓存时间设置为检查间隔的1/3 + self.cache_duration = max(1, config.check_interval // 3) # 初始化时获取所有域名当前的记录 self.init_current_records() @@ -199,50 +206,134 @@ class DNSPodManager: logger.error(f"获取当前记录失败: {str(e)}") return {} - def update_domain_records(self, domain_config: Dict) -> None: - """更新单个域名的记录""" + def check_ip_availability(self, ip: str) -> bool: + """检查IP是否可以ping通""" + # 检查缓存 + now = datetime.now() + if ip in self.ip_availability_cache: + cache_info = self.ip_availability_cache[ip] + if now - cache_info['last_check'] < timedelta(minutes=self.cache_duration): + return cache_info['available'] + + try: + # 根据操作系统选择ping命令参数 + if os.name == 'nt': # Windows系统 + ping_args = ['ping', '-n', '1', '-w', '1000', ip] + else: # Linux/Unix系统 + ping_args = ['ping', '-c', '1', '-W', '1', ip] + + result = subprocess.run(ping_args, + capture_output=True, + text=True) + available = result.returncode == 0 + + # 更新缓存 + self.ip_availability_cache[ip] = { + 'available': available, + 'last_check': now + } + + if not available: + logger.warning(f"IP {ip} ping测试失败,命令输出:{result.stdout if result.stdout else result.stderr}") + return available + except Exception as e: + logger.error(f"Ping测试出错 - IP: {ip}, 错误信息: {str(e)}, 命令参数: {ping_args}") + return False + + def find_best_available_ip(self, ip_data: Dict, ip_version: str) -> Optional[Tuple[str, int]]: + """查找可用且延迟最低的IP,返回(IP, 延迟)""" + if ip_version != "v4": # 只检查IPv4地址 + return self.find_best_ip(ip_data, ip_version) + + # 按延迟排序所有IP地址,并获取优选IP,检查IP可用性,更新不同线路的记录等功能。 + all_ips = [] + for line_key in ["CM", "CU", "CT"]: + if line_key in ip_data[ip_version]: + all_ips.extend(ip_data[ip_version][line_key]) + + # 按延迟排序 + all_ips.sort(key=lambda x: x["latency"]) + + # 查找第一个可用的IP + for ip_info in all_ips: + if self.check_ip_availability(ip_info["ip"]): + return (ip_info["ip"], ip_info["latency"]) + + return None + + def find_line_best_available_ip( + self, ip_data: Dict, ip_version: str, line_key: str + ) -> Optional[Tuple[str, int]]: + """查找指定线路可用且延迟最低的IP""" + if ip_version != "v4": # 只检查IPv4地址 + return self.find_line_best_ip(ip_data, ip_version, line_key) + + if line_key not in ip_data[ip_version]: + return None + + ips = ip_data[ip_version][line_key] + if not ips: + return None + + # 按延迟排序 + ips.sort(key=lambda x: x["latency"]) + + # 查找第一个可用的IP + for ip_info in ips: + if self.check_ip_availability(ip_info["ip"]): + return (ip_info["ip"], ip_info["latency"]) + + return None + def update_domain_records(self, domain_config): + """更新指定域名的记录""" domain = domain_config["domain"] sub_domain = domain_config["sub_domain"] - ttl = domain_config["ttl"] - remark = domain_config["remark"] - - # 获取优选IP数据 - ip_data = self.get_optimal_ips() - if not ip_data: - return + ttl = domain_config.get("ttl", 600) + remark = domain_config.get("remark") # 获取当前记录 - if domain not in self.current_ips: - self.current_ips[domain] = self.get_current_records(domain, sub_domain) - current_records = self.current_ips[domain] + current_records = self.get_current_records(domain, sub_domain) + + # 获取优选IP + ip_data = self.get_optimal_ips() + if not ip_data: + logger.error(f"无法获取优选IP,跳过更新 {domain}") + return # 处理IPv4记录 if domain_config["ipv4_enabled"] and "v4" in ip_data: - # 处理默认线路 - best_ip = self.find_best_ip(ip_data, "v4") - if best_ip: - ip, latency = best_ip - current_ip = current_records.get("默认", {}).get("A") - if current_ip != ip: - logger.info( - f"更新A记录: {domain} - {sub_domain} - 默认 - {ip} (延迟: {latency}ms)" - ) - if self.update_record( - domain, sub_domain, "A", "默认", ip, ttl, remark - ): - # 更新成功后更新缓存 - if "默认" not in current_records: - current_records["默认"] = {} - current_records["默认"]["A"] = ip - time.sleep(1) - - # 更新其他线路的IPv4记录 + # 获取所有线路的最佳IPv4地址 line_mapping = {"移动": "CM", "联通": "CU", "电信": "CT"} + best_ips = {} for line, line_key in line_mapping.items(): - if line_key in ip_data["v4"]: - best_ip = self.find_line_best_ip(ip_data, "v4", line_key) - if best_ip: - ip, latency = best_ip + best_ip = self.find_line_best_available_ip(ip_data, "v4", line_key) + if best_ip: + ip, latency = best_ip + best_ips[line] = (ip, latency) + + # 检查是否所有线路的IPv4地址都相同 + if best_ips: + unique_ips = {ip for ip, _ in best_ips.values()} + if len(unique_ips) == 1: + # 所有线路的IPv4地址相同,只添加默认线路 + ip = list(unique_ips)[0] + min_latency = min(latency for _, latency in best_ips.values()) + current_ip = current_records.get("默认", {}).get("A") + if current_ip != ip: + logger.info( + f"更新A记录: {domain} - {sub_domain} - 默认 - {ip} (延迟: {min_latency}ms) [所有线路IP相同]" + ) + if self.update_record( + domain, sub_domain, "A", "默认", ip, ttl, remark + ): + # 更新成功后更新缓存 + if "默认" not in current_records: + current_records["默认"] = {} + current_records["默认"]["A"] = ip + time.sleep(1) + else: + # IPv4地址不同,需要为每个线路添加记录 + for line, (ip, latency) in best_ips.items(): current_ip = current_records.get(line, {}).get("A") if current_ip != ip: logger.info( @@ -257,17 +348,33 @@ class DNSPodManager: current_records[line]["A"] = ip time.sleep(1) + # 添加默认线路(使用延迟最低的IP) + best_ip = min(best_ips.items(), key=lambda x: x[1][1]) + ip, latency = best_ip[1] + current_ip = current_records.get("默认", {}).get("A") + if current_ip != ip: + logger.info( + f"更新A记录: {domain} - {sub_domain} - 默认 - {ip} (延迟: {latency}ms)" + ) + if self.update_record( + domain, sub_domain, "A", "默认", ip, ttl, remark + ): + # 更新成功后更新缓存 + if "默认" not in current_records: + current_records["默认"] = {} + current_records["默认"]["A"] = ip + time.sleep(1) + # 处理IPv6记录 if domain_config["ipv6_enabled"] and "v6" in ip_data: # 获取所有线路的最佳IPv6地址 line_mapping = {"移动": "CM", "联通": "CU", "电信": "CT"} best_ips = {} for line, line_key in line_mapping.items(): - if line_key in ip_data["v6"]: - best_ip = self.find_line_best_ip(ip_data, "v6", line_key) - if best_ip: - ip, latency = best_ip - best_ips[line] = (ip, latency) + best_ip = self.find_line_best_ip(ip_data, "v6", line_key) + if best_ip: + ip, latency = best_ip + best_ips[line] = (ip, latency) # 检查是否所有线路的IPv6地址都相同 if best_ips: @@ -339,9 +446,8 @@ def main(): manager.check_and_update() # 每5分钟检查一次是否需要更新 - schedule.every(5).minutes.do(manager.check_and_update) - - logger.info("程序启动成功,开始监控更新(每5分钟检查一次)...") + schedule.every(config.check_interval).minutes.do(manager.check_and_update) + logger.info(f"程序启动成功,开始监控更新(每{config.check_interval}分钟检查一次)...") while True: schedule.run_pending() time.sleep(1) diff --git a/readme.md b/readme.md index ac40510..0028764 100644 --- a/readme.md +++ b/readme.md @@ -29,23 +29,28 @@ 1. 创建配置文件: ```bash -cp .env.example .env +cp config.example.yaml config.yaml ``` -2. 编辑 `.env` 文件,填写您的配置: -```ini -# DNSPOD API 配置 -TENCENT_SECRET_ID=your_secret_id_here -TENCENT_SECRET_KEY=your_secret_key_here +2. 编辑 `config.yaml` 文件,填写您的配置: +```yaml +# 腾讯云API配置 +tencent: + secret_id: your_secret_id_here + secret_key: your_secret_key_here -# 域名配置 - 域名1 -DOMAIN_1=example1.com -SUB_DOMAIN_1=@ # 子域名,@ 表示根域名 -REMARK_1=优选IP # 记录备注 -TTL_1=600 # TTL值(秒) -IPV4_ENABLED_1=true # 是否启用IPv4记录 -IPV6_ENABLED_1=true # 是否启用IPv6记录 -ENABLED_1=true # 是否启用此域名配置 +# 日志级别 +log_level: INFO + +# 域名配置列表 +domains: + - domain: example1.com + sub_domain: "@" # 子域名,@ 表示根域名 + remark: 优选IP # 记录备注 + ttl: 600 # TTL值(秒) + ipv4_enabled: true # 是否启用IPv4记录 + ipv6_enabled: true # 是否启用IPv6记录 + enabled: true # 是否启用此域名配置 ``` 3. 拉取并运行容器: @@ -64,8 +69,8 @@ cd dnspod-yxip 2. 创建并编辑配置文件: ```bash -cp .env.example .env -# 编辑 .env 文件 +cp config.example.yaml config.yaml +# 编辑 config 文件 ``` 3. 构建镜像: @@ -81,15 +86,14 @@ docker compose up -d ## 配置说明 每个域名配置包含以下参数: -- `DOMAIN_n`: 域名 -- `SUB_DOMAIN_n`: 子域名,@ 表示根域名,* 表示泛解析 -- `REMARK_n`: 记录备注 -- `TTL_n`: TTL值(秒) -- `IPV4_ENABLED_n`: 是否启用IPv4记录 -- `IPV6_ENABLED_n`: 是否启用IPv6记录 -- `ENABLED_n`: 是否启用此域名配置 +- `DOMAIN`: 域名 +- `SUB_DOMAIN`: 子域名,@ 表示根域名,* 表示泛解析 +- `REMARK`: 记录备注 +- `TTL`: TTL值(秒) +- `IPV4_ENABLED`: 是否启用IPv4记录 +- `IPV6_ENABLED`: 是否启用IPv6记录 +- `ENABLED`: 是否启用此域名配置 -其中 n 是域名编号(1, 2, 3...),可以配置任意数量的域名。 ## 日志查看 diff --git a/requirements.txt b/requirements.txt index dbbf9b0..a8feed0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ requests>=2.31.0 python-dotenv>=1.0.0 schedule>=1.2.1 loguru>=0.7.2 -tencentcloud-sdk-python>=3.0.1000 \ No newline at end of file +tencentcloud-sdk-python>=3.0.1000 +pyyaml>=6.0.2 \ No newline at end of file