适用范围:NAS、家庭服务器、专业服务器。
主要修订的内容为优化了获取IPv4与IPV6地址,部分接口受限造成的获取失败。因此建立了一个IP池这样随机轮询,减少被封的概率。
从新设计了IPv4与IPV6逻辑实现了复用。
当然还是支持IPv4与IPV6的。单文件版本,打包容易。
打包命令如下:首先打开cmd,然后cd到代码地址,然后运行这个命令。
pyinstaller --onefile --hidden-import=queue aliddns.py
#!/usr/bin/env python3
# 实现了阿里云ipv4与ipv6同时维护并且不会覆盖。
# 增强了多接口随地检测地址,防止端口超限制。
import json
import logging
import os
import sys
import socket
from urllib.request import urlopen
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ServerException, ClientException
from aliyunsdkalidns.request.v20150109 import (
DescribeSubDomainRecordsRequest,
AddDomainRecordRequest,
UpdateDomainRecordRequest,
DeleteDomainRecordRequest
)
# 配置日志
def get_log_path():
if getattr(sys, 'frozen', False):
# 如果是打包后的可执行文件
base_dir = os.path.dirname(sys.executable)
else:
# 如果是脚本运行
base_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_dir, 'dns_controller.log')
# 配置日志
log_file = get_log_path()
logging.basicConfig(filename=log_file, level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s')
class DnsController:
def __init__(self):
# 读取配置文件或环境变量
self.access_key_id = os.getenv('ALIYUN_ACCESS_KEY_ID', "你自己的")
self.access_key_secret = os.getenv('ALIYUN_ACCESS_KEY_SECRET', "你自己的")
self.region = "cn-shanghai"
self.domain = "你自己的.xyz"
self.name_ipv4 = ["你自己的二级", "你自己的二级"]
self.name_ipv6 = ["你自己的二级", "你自己的二级"]
self.record_type_ipv4 = "A"
self.record_type_ipv6 = "AAAA"
# IPv4 API列表
self.ipv4_api_list = [
('https://api.ipify.org?format=json', 'ip'),
('https://httpbin.org/ip', 'origin'),
('https://v4.ident.me/.json', 'address'),
('http://ip-api.com/json/?fields=query', 'query')
]
# IPv6 API列表
self.ipv6_api_list = [
('https://ipv6.jsonip.com/', 'ip'),
('https://v6.ident.me/.json', 'address'),
('https://ifconfig.me/all.json', 'ip_addr'),
('https://wtfismyip.com/json', 'YourFuckingIPAddress')
]
self.timeout = 10 # 请求超时时间(秒)
self.client = AcsClient(
self.access_key_id,
self.access_key_secret,
self.region
)
def _validate_ipv4(self, ip):
"""简单验证IPv4格式"""
parts = ip.split('.')
if len(parts) != 4:
return False
return all(part.isdigit() and 0 <= int(part) <= 255 for part in parts)
def _validate_ipv6(self, ip):
"""简单验证IPv6格式"""
try:
socket.inet_pton(socket.AF_INET6, ip)
return True
except socket.error:
return False
def _get_ip(self, api_list, ip_type='ipv4'):
"""通过多个API获取IP地址,自动故障转移"""
errors = []
for url, key in api_list:
try:
with urlopen(url, timeout=self.timeout) as response:
data = json.loads(response.read().decode())
# 处理特殊响应格式
if key == 'origin': # httpbin.org
ip = data[key].split(', ')[0]
else:
ip = data[key]
# 验证IP
if (ip_type == 'ipv4' and self._validate_ipv4(ip)) or \
(ip_type == 'ipv6' and self._validate_ipv6(ip)):
logging.info(f"成功通过 {url} 获取{ip_type.upper()}地址: {ip}")
return ip
except Exception as e:
error_message = f"API {url} 请求失败: {str(e)}"
logging.warning(error_message)
errors.append(error_message)
continue
raise RuntimeError(f"所有{ip_type.upper()}查询API均不可用\n" + "\n".join(errors))
def add(self, DomainName, RR, Type, Value):
"""添加新的域名解析记录"""
try:
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')
request.set_DomainName(DomainName)
request.set_RR(RR)
request.set_Type(Type)
request.set_Value(Value)
response = self.client.do_action_with_exception(request)
logging.info(f"成功添加记录: {RR}.{DomainName} ({Type}) -> {Value}")
return True
except Exception as e:
logging.error(f"添加域名解析记录失败: {e}", exc_info=True)
return False
def update(self, RecordId, RR, Type, Value):
"""修改域名解析记录"""
try:
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')
request.set_RecordId(RecordId)
request.set_RR(RR)
request.set_Type(Type)
request.set_Value(Value)
response = self.client.do_action_with_exception(request)
logging.info(f"成功更新记录: {RR}.{self.domain} ({Type}) -> {Value}")
return True
except Exception as e:
logging.error(f"修改域名解析记录失败: {e}", exc_info=True)
return False
def delete(self, RecordId):
"""删除域名解析记录"""
try:
delete_request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
delete_request.set_accept_format('json')
delete_request.set_RecordId(RecordId)
self.client.do_action_with_exception(delete_request)
logging.info(f"成功删除记录: {RecordId}")
return True
except Exception as e:
logging.error(f"删除域名解析记录失败: {e}", exc_info=True)
return False
def process_dns_records(self, ip_type='ipv4'):
"""处理DNS记录的主逻辑"""
try:
# 获取IP地址
ip = self._get_ip(getattr(self, f'{ip_type}_api_list'), ip_type)
record_type = getattr(self, f'record_type_{ip_type}')
subdomains = getattr(self, f'name_{ip_type}')
print(f"当前公网{ip_type.upper()}地址: {ip}")
# 初始化请求对象
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')
request.set_DomainName(self.domain)
# 处理每个子域名
for subdomain in subdomains:
full_subdomain = f"{subdomain}.{self.domain}"
print(f"正在处理子域名: {full_subdomain}")
# 查询记录
request.set_SubDomain(full_subdomain)
response = self.client.do_action_with_exception(request)
records = json.loads(response)
# 记录处理逻辑
if records['TotalCount'] == 0:
self.add(self.domain, subdomain, record_type, ip)
print(f"创建新记录: {full_subdomain} → {ip}")
else:
type_records = [
r for r in records['DomainRecords']['Record']
if r['Type'] == record_type
]
if not type_records:
self.add(self.domain, subdomain, record_type, ip)
print(f"添加{record_type}记录: {full_subdomain} → {ip}")
else:
need_update = False
for record in type_records:
if record['Value'].strip() != ip:
self.update(record['RecordId'], subdomain, record_type, ip)
print(f"更新记录: {full_subdomain} ({record['RecordId']}) → {ip}")
need_update = True
if not need_update:
print(f"记录无需更新: {full_subdomain}")
except (ServerException, ClientException) as e:
logging.error(f"阿里云API错误: {e.get_error_code()} - {e.get_error_msg()}")
print(f"阿里云API错误: {e.get_error_code()} - {e.get_error_msg()}")
except Exception as e:
logging.error(f"处理过程中发生未预期错误: {str(e)}", exc_info=True)
print(f"处理过程中发生未预期错误: {str(e)}")
if __name__ == "__main__":
controller = DnsController()
controller.process_dns_records('ipv4')
controller.process_dns_records('ipv6')