mirror of
https://github.com/rachelos/we-mp-rss.git
synced 2026-08-28 17:21:00 +08:00
564 lines
19 KiB
Python
564 lines
19 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
批量检测 DNS 解析记录脚本
|
|
|
|
支持功能:
|
|
- 批量检测多个域名的 DNS 记录
|
|
- 支持多种记录类型:A, AAAA, CNAME, MX, TXT, NS, SOA
|
|
- 支持指定自定义 DNS 服务器
|
|
- 支持并发检测
|
|
- 支持从文件读取域名列表
|
|
- 输出 JSON 或表格格式结果
|
|
|
|
使用方法:
|
|
python dns.py example.com
|
|
python dns.py example.com google.com
|
|
python dns.py -f domains.txt
|
|
python dns.py example.com -t A,MX,TXT
|
|
python dns.py example.com -s 8.8.8.8
|
|
python dns.py example.com -o json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import socket
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from dataclasses import dataclass, asdict
|
|
from typing import Optional
|
|
|
|
try:
|
|
import dns.resolver
|
|
import dns.exception
|
|
HAS_DNSPYTHON = True
|
|
except ImportError:
|
|
HAS_DNSPYTHON = False
|
|
|
|
|
|
@dataclass
|
|
class DNSRecord:
|
|
"""DNS 记录数据结构"""
|
|
domain: str
|
|
record_type: str
|
|
value: str
|
|
ttl: Optional[int] = None
|
|
|
|
|
|
@dataclass
|
|
class DNSResult:
|
|
"""DNS 查询结果"""
|
|
domain: str
|
|
record_type: str
|
|
records: list
|
|
error: Optional[str] = None
|
|
query_time: float = 0.0
|
|
|
|
|
|
def query_a_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 A 记录(IPv4 地址)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'A')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='A',
|
|
value=str(rdata),
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
try:
|
|
# 使用 socket 进行基本查询
|
|
addr_info = socket.getaddrinfo(domain, None, socket.AF_INET)
|
|
seen = set()
|
|
for info in addr_info:
|
|
ip = info[4][0]
|
|
if ip not in seen:
|
|
seen.add(ip)
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='A',
|
|
value=ip
|
|
))
|
|
except socket.gaierror as e:
|
|
error = str(e)
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='A',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_aaaa_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 AAAA 记录(IPv6 地址)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'AAAA')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='AAAA',
|
|
value=str(rdata),
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
try:
|
|
addr_info = socket.getaddrinfo(domain, None, socket.AF_INET6)
|
|
seen = set()
|
|
for info in addr_info:
|
|
ip = info[4][0]
|
|
if ip not in seen:
|
|
seen.add(ip)
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='AAAA',
|
|
value=ip
|
|
))
|
|
except socket.gaierror as e:
|
|
error = str(e)
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='AAAA',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_cname_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 CNAME 记录(别名)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'CNAME')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='CNAME',
|
|
value=str(rdata).rstrip('.'),
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
error = "需要安装 dnspython 库来查询 CNAME 记录: pip install dnspython"
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='CNAME',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_mx_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 MX 记录(邮件交换)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'MX')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='MX',
|
|
value=f"{rdata.preference} {str(rdata.exchange).rstrip('.')}",
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
error = "需要安装 dnspython 库来查询 MX 记录: pip install dnspython"
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='MX',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_txt_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 TXT 记录"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'TXT')
|
|
for rdata in answers:
|
|
txt_data = ''.join(s.decode() for s in rdata.strings)
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='TXT',
|
|
value=txt_data,
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
error = "需要安装 dnspython 库来查询 TXT 记录: pip install dnspython"
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='TXT',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_ns_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 NS 记录(域名服务器)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'NS')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='NS',
|
|
value=str(rdata).rstrip('.'),
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
error = "需要安装 dnspython 库来查询 NS 记录: pip install dnspython"
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='NS',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
def query_soa_record(domain: str, dns_server: Optional[str] = None) -> DNSResult:
|
|
"""查询 SOA 记录(授权起始)"""
|
|
start_time = time.time()
|
|
records = []
|
|
error = None
|
|
|
|
if HAS_DNSPYTHON:
|
|
try:
|
|
resolver = dns.resolver.Resolver()
|
|
if dns_server:
|
|
resolver.nameservers = [dns_server]
|
|
answers = resolver.resolve(domain, 'SOA')
|
|
for rdata in answers:
|
|
records.append(DNSRecord(
|
|
domain=domain,
|
|
record_type='SOA',
|
|
value=f"{rdata.mname.rstrip('.')} {rdata.rname.rstrip('.')} {rdata.serial} {rdata.refresh} {rdata.retry} {rdata.expire} {rdata.minimum}",
|
|
ttl=answers.rrset.ttl if hasattr(answers, 'rrset') else None
|
|
))
|
|
except dns.exception.DNSException as e:
|
|
error = str(e)
|
|
else:
|
|
error = "需要安装 dnspython 库来查询 SOA 记录: pip install dnspython"
|
|
|
|
return DNSResult(
|
|
domain=domain,
|
|
record_type='SOA',
|
|
records=[asdict(r) for r in records],
|
|
error=error,
|
|
query_time=time.time() - start_time
|
|
)
|
|
|
|
|
|
# 记录类型与查询函数的映射
|
|
QUERY_FUNCTIONS = {
|
|
'A': query_a_record,
|
|
'AAAA': query_aaaa_record,
|
|
'CNAME': query_cname_record,
|
|
'MX': query_mx_record,
|
|
'TXT': query_txt_record,
|
|
'NS': query_ns_record,
|
|
'SOA': query_soa_record,
|
|
}
|
|
|
|
|
|
def check_domain(domain: str, record_types: list, dns_server: Optional[str] = None) -> list:
|
|
"""检查单个域名的多种记录类型"""
|
|
results = []
|
|
for rtype in record_types:
|
|
rtype = rtype.upper()
|
|
if rtype in QUERY_FUNCTIONS:
|
|
result = QUERY_FUNCTIONS[rtype](domain, dns_server)
|
|
results.append(asdict(result))
|
|
else:
|
|
results.append(asdict(DNSResult(
|
|
domain=domain,
|
|
record_type=rtype,
|
|
records=[],
|
|
error=f"不支持的记录类型: {rtype}"
|
|
)))
|
|
return results
|
|
|
|
|
|
def check_domains_batch(domains: list, record_types: list, dns_server: Optional[str] = None,
|
|
max_workers: int = 10) -> dict:
|
|
"""批量检查多个域名"""
|
|
all_results = {}
|
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
future_to_domain = {
|
|
executor.submit(check_domain, domain, record_types, dns_server): domain
|
|
for domain in domains
|
|
}
|
|
|
|
for future in as_completed(future_to_domain):
|
|
domain = future_to_domain[future]
|
|
try:
|
|
results = future.result()
|
|
all_results[domain] = results
|
|
except Exception as e:
|
|
all_results[domain] = [asdict(DNSResult(
|
|
domain=domain,
|
|
record_type='ALL',
|
|
records=[],
|
|
error=f"查询失败: {str(e)}"
|
|
))]
|
|
|
|
return all_results
|
|
|
|
|
|
def read_domains_from_file(filepath: str) -> list:
|
|
"""从文件读取域名列表"""
|
|
domains = []
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
domain = line.strip()
|
|
# 跳过空行和注释行
|
|
if domain and not domain.startswith('#'):
|
|
# 移除可能的 http:// 或 https:// 前缀
|
|
domain = domain.replace('http://', '').replace('https://', '')
|
|
# 移除路径部分
|
|
domain = domain.split('/')[0]
|
|
domains.append(domain)
|
|
except FileNotFoundError:
|
|
print(f"错误: 文件 '{filepath}' 不存在")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"读取文件错误: {e}")
|
|
sys.exit(1)
|
|
return domains
|
|
|
|
|
|
def print_table(results: dict):
|
|
"""以表格格式输出结果"""
|
|
print("\n" + "=" * 80)
|
|
print(f"{'域名':<30} {'类型':<8} {'记录值':<40}")
|
|
print("=" * 80)
|
|
|
|
for domain, domain_results in results.items():
|
|
first_row = True
|
|
for result in domain_results:
|
|
if result['records']:
|
|
for i, record in enumerate(result['records']):
|
|
domain_display = domain if (first_row and i == 0) else ''
|
|
type_display = result['record_type'] if (i == 0) else ''
|
|
value = record['value'][:38] + '..' if len(record['value']) > 40 else record['value']
|
|
print(f"{domain_display:<30} {type_display:<8} {value:<40}")
|
|
if i == 0 and result['records']:
|
|
first_row = False
|
|
elif result['error']:
|
|
domain_display = domain if first_row else ''
|
|
print(f"{domain_display:<30} {result['record_type']:<8} [错误] {result['error'][:30]}")
|
|
first_row = False
|
|
else:
|
|
domain_display = domain if first_row else ''
|
|
print(f"{domain_display:<30} {result['record_type']:<8} [无记录]")
|
|
first_row = False
|
|
print("-" * 80)
|
|
|
|
|
|
def print_summary(results: dict):
|
|
"""输出统计摘要"""
|
|
total_domains = len(results)
|
|
total_queries = sum(len(v) for v in results.values())
|
|
success_queries = sum(1 for v in results.values() for r in v if r['records'])
|
|
error_queries = sum(1 for v in results.values() for r in v if r['error'])
|
|
total_time = sum(r['query_time'] for v in results.values() for r in v)
|
|
|
|
print("\n" + "=" * 80)
|
|
print("统计摘要")
|
|
print("=" * 80)
|
|
print(f"检测域名数: {total_domains}")
|
|
print(f"查询总次数: {total_queries}")
|
|
print(f"成功查询: {success_queries}")
|
|
print(f"失败查询: {error_queries}")
|
|
print(f"总耗时: {total_time:.2f} 秒")
|
|
print("=" * 80)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='批量检测 DNS 解析记录',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
示例:
|
|
python dns.py example.com
|
|
python dns.py example.com google.com
|
|
python dns.py -f domains.txt
|
|
python dns.py example.com -t A,MX,TXT
|
|
python dns.py example.com -s 8.8.8.8
|
|
python dns.py example.com -o json
|
|
python dns.py example.com -w 20
|
|
# 使用前缀和后缀批量生成域名
|
|
python dns.py --suffix example.com --prefix www,api,blog
|
|
python dns.py --suffix example.com --prefix-file prefixes.txt
|
|
# 直接检测所有子域名
|
|
python dns.py www.example.com api.example.com blog.example.com
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('domains', nargs='*', help='要检测的域名列表')
|
|
parser.add_argument('-f', '--file', help='从文件读取域名列表(每行一个域名)')
|
|
parser.add_argument('--suffix', '-S', help='统一域名后缀,与 --prefix 或 --prefix-file 配合使用')
|
|
parser.add_argument('--prefix', '-P', help='域名前缀列表,多个用逗号分隔,如: www,api,blog')
|
|
parser.add_argument('--prefix-file', '-PF', help='从文件读取域名前缀列表(每行一个)')
|
|
parser.add_argument('-t', '--types', default='A',
|
|
help='DNS 记录类型,多个用逗号分隔 (默认: A),支持: A,AAAA,CNAME,MX,TXT,NS,SOA')
|
|
parser.add_argument('-s', '--server', help='指定 DNS 服务器地址')
|
|
parser.add_argument('-o', '--output', choices=['table', 'json'], default='table',
|
|
help='输出格式 (默认: table)')
|
|
parser.add_argument('-w', '--workers', type=int, default=10,
|
|
help='并发工作线程数 (默认: 10)')
|
|
parser.add_argument('--summary', action='store_true', help='显示统计摘要')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# 收集域名
|
|
domains = list(args.domains)
|
|
if args.file:
|
|
domains.extend(read_domains_from_file(args.file))
|
|
|
|
# 处理前缀+后缀生成域名
|
|
if args.suffix:
|
|
suffix = args.suffix.strip().lstrip('.')
|
|
prefixes = []
|
|
|
|
# 从命令行获取前缀
|
|
if args.prefix:
|
|
prefixes.extend([p.strip() for p in args.prefix.split(',') if p.strip()])
|
|
|
|
# 从文件读取前缀
|
|
if args.prefix_file:
|
|
try:
|
|
with open(args.prefix_file, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
prefix = line.strip()
|
|
if prefix and not prefix.startswith('#'):
|
|
prefixes.append(prefix)
|
|
except FileNotFoundError:
|
|
print(f"错误: 前缀文件 '{args.prefix_file}' 不存在")
|
|
sys.exit(1)
|
|
|
|
# 生成域名
|
|
if prefixes:
|
|
for prefix in prefixes:
|
|
if prefix in ('@', ''):
|
|
# @ 或空前缀表示根域名
|
|
domains.append(suffix)
|
|
else:
|
|
domains.append(f"{prefix}.{suffix}")
|
|
else:
|
|
# 只有后缀没有前缀,只检测根域名
|
|
domains.append(suffix)
|
|
|
|
if not domains:
|
|
parser.print_help()
|
|
print("\n错误: 请提供至少一个域名,或使用以下方式之一:")
|
|
print(" - 直接指定域名: python dns.py example.com")
|
|
print(" - 从文件读取: python dns.py -f domains.txt")
|
|
print(" - 前缀+后缀: python dns.py --suffix example.com --prefix www,api")
|
|
sys.exit(1)
|
|
|
|
# 解析记录类型
|
|
record_types = [t.strip().upper() for t in args.types.split(',')]
|
|
|
|
# 检查 dnspython 库
|
|
if not HAS_DNSPYTHON:
|
|
unsupported = [t for t in record_types if t != 'A' and t != 'AAAA']
|
|
if unsupported:
|
|
print(f"警告: 检测到未安装 dnspython 库,以下记录类型将无法查询: {', '.join(unsupported)}")
|
|
print("建议安装: pip install dnspython")
|
|
print()
|
|
|
|
# 输出配置信息
|
|
print(f"\nDNS 解析检测工具")
|
|
print(f"{'=' * 40}")
|
|
print(f"域名数量: {len(domains)}")
|
|
print(f"记录类型: {', '.join(record_types)}")
|
|
print(f"DNS 服务器: {args.server or '系统默认'}")
|
|
print(f"并发线程: {args.workers}")
|
|
print(f"{'=' * 40}\n")
|
|
|
|
# 执行检测
|
|
results = check_domains_batch(domains, record_types, args.server, args.workers)
|
|
|
|
# 输出结果
|
|
if args.output == 'json':
|
|
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
else:
|
|
print_table(results)
|
|
|
|
# 输出摘要
|
|
if args.summary:
|
|
print_summary(results)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|