first commit

This commit is contained in:
wood chen 2025-01-12 03:47:20 +08:00
commit bb4d8473db
9 changed files with 605 additions and 0 deletions

42
.env.example Normal file
View File

@ -0,0 +1,42 @@
# DNSPOD API 配置
DNSPOD_ID=your_id_here
DNSPOD_TOKEN=your_token_here
# 域名配置 - 域名1
DOMAIN_1=example1.com
SUB_DOMAIN_1=@ # 子域名,@ 表示根域名
REMARK_1=优选IP # 记录备注
TTL_1=600 # TTL值
UPDATE_INTERVAL_1=15 # 更新间隔(分钟)
IPV4_ENABLED_1=true # 是否启用IPv4记录
IPV6_ENABLED_1=true # 是否启用IPv6记录
ENABLED_1=true # 是否启用此域名配置
# 域名配置 - 域名2更频繁的更新
DOMAIN_2=example2.com
SUB_DOMAIN_2=www
REMARK_2=CF优选 # 记录备注
TTL_2=300 # 更短的TTL
UPDATE_INTERVAL_2=5 # 更频繁的更新
IPV4_ENABLED_2=true # 只启用IPv4
IPV6_ENABLED_2=false # 不启用IPv6
ENABLED_2=true
# 域名配置 - 域名3更长的缓存
DOMAIN_3=example3.com
SUB_DOMAIN_3=*
REMARK_3=CloudFlare优选 # 记录备注
TTL_3=1800 # 更长的TTL
UPDATE_INTERVAL_3=30 # 更长的更新间隔
IPV4_ENABLED_3=true # 启用IPv4
IPV6_ENABLED_3=true # 启用IPv6
ENABLED_3=true
# 可以继续添加更多域名配置...
# 每个域名可以独立控制:
# - 是否启用IPv4 (IPV4_ENABLED_n)
# - 是否启用IPv6 (IPV6_ENABLED_n)
# - TTL值 (TTL_n)
# - 更新间隔 (UPDATE_INTERVAL_n)
# - 是否启用 (ENABLED_n)
# - 记录备注 (REMARK_n)

40
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Docker
on:
push:
branches:
- main
tags:
- v*
env:
IMAGE_NAME: dnspod-yxip
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: woodchen
password: ${{ secrets.ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
tags: woodchen/${{ env.IMAGE_NAME }}:latest
platforms: linux/amd64,linux/arm64

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/__pycache__

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
# 复制项目文件
COPY requirements.txt .
COPY config.py .
COPY main.py .
# 安装依赖
RUN pip install -r requirements.txt
# 创建配置文件和日志目录
RUN mkdir -p logs && touch .env
# 运行程序
CMD ["python", "main.py"]

93
config.py Normal file
View File

@ -0,0 +1,93 @@
import os
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# DNSPOD API配置
DNSPOD_ID = os.getenv("DNSPOD_ID")
DNSPOD_TOKEN = os.getenv("DNSPOD_TOKEN")
# API接口配置
API_URL = "https://api.vvhan.com/tool/cf_ip"
# 默认配置
DEFAULT_TTL = 600
DEFAULT_UPDATE_INTERVAL = 15
# 支持的线路类型
LINE_TYPES = ["默认", "移动", "联通", "电信"]
# 支持的记录类型
RECORD_TYPES = ["A", "AAAA"]
# 域名配置
DOMAINS = []
# 从环境变量加载域名配置
i = 1
while True:
domain_key = f"DOMAIN_{i}"
if not os.getenv(domain_key):
break
# 获取基本配置
base_config = {
"domain": os.getenv(domain_key),
"sub_domain": os.getenv(f"SUB_DOMAIN_{i}", "@"),
"line": LINE_TYPES,
"ttl": int(os.getenv(f"TTL_{i}", str(DEFAULT_TTL))),
"update_interval": int(
os.getenv(f"UPDATE_INTERVAL_{i}", str(DEFAULT_UPDATE_INTERVAL))
),
"enabled": os.getenv(f"ENABLED_{i}", "true").lower() == "true",
"remark": os.getenv(f"REMARK_{i}", "YXIP"),
}
# 检查是否启用IPv4
ipv4_enabled = os.getenv(f"IPV4_ENABLED_{i}", "true").lower() == "true"
# 检查是否启用IPv6
ipv6_enabled = os.getenv(f"IPV6_ENABLED_{i}", "true").lower() == "true"
# 为每个启用的记录类型创建配置
if ipv4_enabled:
ipv4_config = base_config.copy()
ipv4_config["record_type"] = "A"
DOMAINS.append(ipv4_config)
if ipv6_enabled:
ipv6_config = base_config.copy()
ipv6_config["record_type"] = "AAAA"
DOMAINS.append(ipv6_config)
i += 1
# 如果没有配置任何域名,使用默认配置
if not DOMAINS:
base_config = {
"domain": os.getenv("DOMAIN", "example.com"),
"sub_domain": os.getenv("SUB_DOMAIN", "@"),
"line": LINE_TYPES,
"ttl": int(os.getenv("TTL", str(DEFAULT_TTL))),
"update_interval": int(
os.getenv("UPDATE_INTERVAL", str(DEFAULT_UPDATE_INTERVAL))
),
"enabled": True,
}
# 检查默认的IPv4和IPv6设置
ipv4_enabled = os.getenv("IPV4_ENABLED", "true").lower() == "true"
ipv6_enabled = os.getenv("IPV6_ENABLED", "true").lower() == "true"
if ipv4_enabled:
ipv4_config = base_config.copy()
ipv4_config["record_type"] = "A"
DOMAINS.append(ipv4_config)
if ipv6_enabled:
ipv6_config = base_config.copy()
ipv6_config["record_type"] = "AAAA"
DOMAINS.append(ipv6_config)
# 日志配置
LOG_LEVEL = "INFO"

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
services:
dnspod-yxip:
image: woodchen/dnspod-yxip:latest
container_name: dnspod-yxip
restart: always
volumes:
- ./.env:/app/.env:ro # 映射环境变量文件
- ./logs:/app/logs # 映射日志目录

282
main.py Normal file
View File

@ -0,0 +1,282 @@
import json
import time
import requests
import schedule
from loguru import logger
from typing import Dict, List, Optional, Tuple
import config
# 配置日志
logger.add("dnspod.log", rotation="10 MB", level=config.LOG_LEVEL)
class DNSPodManager:
def __init__(self):
self.api_url = "https://dnsapi.cn"
self.common_params = {
"login_token": f"{config.DNSPOD_ID},{config.DNSPOD_TOKEN}",
"format": "json",
"lang": "cn",
"error_on_empty": "no",
}
# 记录每个域名每种记录类型的最后更新时间
self.last_update = {} # 格式: {domain: {'A': timestamp, 'AAAA': timestamp}}
def _api_request(self, path: str, data: Dict) -> Dict:
"""发送API请求"""
try:
url = f"{self.api_url}/{path}"
response = requests.post(url, data={**self.common_params, **data})
result = response.json()
if int(result.get("status", {}).get("code", -1)) != 1:
raise Exception(result.get("status", {}).get("message", "未知错误"))
return result
except Exception as e:
logger.error(f"API请求失败: {str(e)}")
raise
def get_optimal_ips(self) -> Dict:
"""获取优选IP"""
try:
response = requests.get(config.API_URL)
data = response.json()
if data.get("success"):
return data["data"]
raise Exception("API返回数据格式错误")
except Exception as e:
logger.error(f"获取优选IP失败: {str(e)}")
return None
def find_best_ip(self, ip_data: Dict, ip_version: str) -> Optional[Tuple[str, int]]:
"""查找延迟最低的IP返回(IP, 延迟)"""
best_ip = None
min_latency = float("inf")
# 遍历所有线路
for line_key in ["CM", "CU", "CT"]:
if line_key in ip_data[ip_version]:
ips = ip_data[ip_version][line_key]
for ip_info in ips:
if ip_info["latency"] < min_latency:
min_latency = ip_info["latency"]
best_ip = (ip_info["ip"], ip_info["latency"])
return best_ip
def find_line_best_ip(
self, ip_data: Dict, ip_version: str, line_key: str
) -> Optional[Tuple[str, int]]:
"""查找指定线路延迟最低的IP"""
if line_key not in ip_data[ip_version]:
return None
ips = ip_data[ip_version][line_key]
if not ips:
return None
best_ip = min(ips, key=lambda x: x["latency"])
return (best_ip["ip"], best_ip["latency"])
def get_record_list(self, domain: str) -> List:
"""获取域名记录列表"""
try:
result = self._api_request("Record.List", {"domain": domain})
return result.get("records", [])
except Exception as e:
logger.error(f"获取记录列表失败: {str(e)}")
return []
def delete_record(self, domain: str, record_id: str) -> bool:
"""删除DNS记录"""
try:
self._api_request(
"Record.Remove", {"domain": domain, "record_id": record_id}
)
return True
except Exception as e:
logger.error(f"删除记录失败: {str(e)}")
return False
def handle_record_conflicts(self, domain: str, sub_domain: str, record_type: str):
"""处理记录冲突"""
records = self.get_record_list(domain)
for record in records:
# 如果是要添加A记录需要删除CNAME记录
if (
record_type == "A"
and record["type"] == "CNAME"
and record["name"] == sub_domain
):
logger.info(f"删除冲突的CNAME记录: {domain} - {sub_domain}")
self.delete_record(domain, record["id"])
# 如果是要添加CNAME记录需要删除A记录
elif (
record_type == "CNAME"
and record["type"] == "A"
and record["name"] == sub_domain
):
logger.info(f"删除冲突的A记录: {domain} - {sub_domain}")
self.delete_record(domain, record["id"])
def update_record(
self,
domain: str,
sub_domain: str,
record_type: str,
line: str,
value: str,
ttl: int,
remark: str = "YXIP",
) -> bool:
"""更新DNS记录"""
try:
# 处理记录冲突
self.handle_record_conflicts(domain, sub_domain, record_type)
# 获取域名记录列表
records = self.get_record_list(domain)
# 查找匹配的记录
record_id = None
for record in records:
if (
record["name"] == sub_domain
and record["line"] == line
and record["type"] == record_type
):
record_id = record["id"]
break
# 更新或创建记录
data = {
"domain": domain,
"sub_domain": sub_domain,
"record_type": record_type,
"record_line": line,
"value": value,
"ttl": ttl,
"remark": remark,
}
if record_id:
data["record_id"] = record_id
self._api_request("Record.Modify", data)
else:
self._api_request("Record.Create", data)
return True
except Exception as e:
logger.error(f"更新DNS记录失败: {str(e)}")
return False
def update_domain_records(self, domain_config: Dict) -> None:
"""更新单个域名的记录"""
domain = domain_config["domain"]
sub_domain = domain_config["sub_domain"]
record_type = domain_config["record_type"]
ttl = domain_config["ttl"]
remark = domain_config["remark"]
# 获取优选IP数据
ip_data = self.get_optimal_ips()
if not ip_data:
return
# 获取对应版本的IP数据
ip_version = "v6" if record_type == "AAAA" else "v4"
if ip_version not in ip_data:
logger.warning(
f"未找到{ip_version}版本的IP数据跳过更新 {domain}{record_type} 记录"
)
return
# 检查是否有可用的IP数据
has_ip_data = False
for line_key in ["CM", "CU", "CT"]:
if line_key in ip_data[ip_version] and ip_data[ip_version][line_key]:
has_ip_data = True
break
if not has_ip_data:
logger.warning(
f"没有可用的{ip_version}版本IP数据跳过更新 {domain}{record_type} 记录"
)
return
# 先处理默认线路
best_ip = self.find_best_ip(ip_data, ip_version)
if best_ip:
ip, latency = best_ip
logger.info(
f"更新{record_type}记录: {domain} - {sub_domain} - 默认 - {ip} (延迟: {latency}ms)"
)
self.update_record(domain, sub_domain, record_type, "默认", ip, ttl, remark)
# 更新其他线路的记录
for line in domain_config["line"]:
if line == "默认":
continue
if line == "移动":
line_key = "CM"
elif line == "联通":
line_key = "CU"
elif line == "电信":
line_key = "CT"
else:
continue
if line_key in ip_data[ip_version]:
best_ip = self.find_line_best_ip(ip_data, ip_version, line_key)
if best_ip:
ip, latency = best_ip
logger.info(
f"更新{record_type}记录: {domain} - {sub_domain} - {line} - {ip} (延迟: {latency}ms)"
)
self.update_record(
domain, sub_domain, record_type, line, ip, ttl, remark
)
def check_and_update(self):
"""检查并更新所有域名"""
current_time = time.time()
for domain_config in config.DOMAINS:
if not domain_config["enabled"]:
continue
domain = domain_config["domain"]
record_type = domain_config["record_type"]
update_interval = domain_config["update_interval"] * 60 # 转换为秒
# 初始化域名的更新时间记录
if domain not in self.last_update:
self.last_update[domain] = {}
# 获取该记录类型的最后更新时间
last_update = self.last_update[domain].get(record_type, 0)
# 检查是否需要更新
if current_time - last_update >= update_interval:
logger.info(f"开始更新域名: {domain}{record_type} 记录")
self.update_domain_records(domain_config)
self.last_update[domain][record_type] = current_time
def main():
manager = DNSPodManager()
# 首次运行,更新所有域名
manager.check_and_update()
# 每分钟检查一次是否需要更新
schedule.every(1).minutes.do(manager.check_and_update)
while True:
schedule.run_pending()
time.sleep(1)
if __name__ == "__main__":
main()

118
readme.md Normal file
View File

@ -0,0 +1,118 @@
# DNSPOD 优选域名
根据API接口返回的IP地址, 在DNSPOD中创建或者更新域名的解析记录, 支持分线路和多权重。
支持docker运行, 需要配置DNSPOD的ID和TOKEN, 可以配置多个域名。默认每15分钟自动获取IP地址并更新DNSPOD。
## 特性
- 自动获取优选IP地址
- 支持IPv4和IPv6
- 支持移动、联通、电信三个线路
- 自动选择延迟最低的IP
- 支持多域名配置
- 可配置的更新间隔和TTL
- 完整的日志记录
- Docker支持
## 快速开始
### 使用 Docker Compose
1. 创建配置文件:
```bash
cp .env.example .env
```
2. 编辑 `.env` 文件,填写您的配置:
```ini
# DNSPOD API 配置
DNSPOD_ID=your_id_here
DNSPOD_TOKEN=your_token_here
# 域名配置 - 域名1
DOMAIN_1=example1.com
SUB_DOMAIN_1=@ # 子域名,@ 表示根域名
REMARK_1=优选IP # 记录备注
TTL_1=600 # TTL值
UPDATE_INTERVAL_1=15 # 更新间隔(分钟)
IPV4_ENABLED_1=true # 是否启用IPv4记录
IPV6_ENABLED_1=true # 是否启用IPv6记录
ENABLED_1=true # 是否启用此域名配置
```
3. 拉取并运行容器:
```bash
docker compose pull
docker compose up -d
```
### 手动构建运行
1. 克隆仓库:
```bash
git clone https://github.com/your-username/dnspod-yxip.git
cd dnspod-yxip
```
2. 创建并编辑配置文件:
```bash
cp .env.example .env
# 编辑 .env 文件
```
3. 构建镜像:
```bash
docker compose build
```
4. 运行容器:
```bash
docker compose up -d
```
## 配置说明
每个域名配置包含以下参数:
- `DOMAIN_n`: 域名
- `SUB_DOMAIN_n`: 子域名,@ 表示根域名,* 表示泛解析
- `REMARK_n`: 记录备注
- `TTL_n`: TTL值
- `UPDATE_INTERVAL_n`: 更新间隔(分钟)
- `IPV4_ENABLED_n`: 是否启用IPv4记录
- `IPV6_ENABLED_n`: 是否启用IPv6记录
- `ENABLED_n`: 是否启用此域名配置
其中 n 是域名编号1, 2, 3...),可以配置任意数量的域名。
## 日志查看
日志文件保存在 `logs` 目录下:
```bash
# 查看实时日志
docker compose logs -f
# 查看日志文件
cat logs/dnspod.log
```
## 更新
1. 拉取最新镜像:
```bash
docker compose pull
```
2. 重新启动容器:
```bash
docker compose up -d
```
## 贡献
欢迎提交 Issue 和 Pull Request
## 许可证
MIT License

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
requests>=2.31.0
python-dotenv>=1.0.0
schedule>=1.2.1
loguru>=0.7.2