Files
2026-03-25 22:58:29 +08:00

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()