前几天晚上我盯着一份三千多行的域名列表发呆任务是把它全部解析到IP再按网段和用途归类。刚开始天真地以为写个脚本循环调解析接口就完事了结果跑完一等就是四十多分钟还动不动卡住某个域名解析超时就能让整个脚本停在那里干等。后来我把方案拆成两层底层用massdns来一波海量粗筛上层用aiodns嵌入Python流程做精细查询总算把时间压缩到了几十秒还顺手解决了一堆之前没注意过的稳定性问题。这篇文章就把这套“组合拳”完整拆开讲包括为什么传统方式慢、massdns怎么配置才能发挥真实力、aiodns怎么和现有代码无缝配合以及我在实际使用中踩过的限速、丢包、泛解析等坑。适合被批量域名解析折磨过的运维、安全测试和数据分析相关的朋友如果你手里有几千甚至几万个域名需要做成资产台账文中的思路可以直接拿过去用。1. 批量解析的第一步先搞清楚系统自带解析器为什么慢1.1 一个真实的需求场景三千个域名要在一顿饭时间内完成解析那次任务的原始输入很朴素一份域名清单大约三千多行来源是日志里积攒下来的访问域名和内部系统登记的老域名混在一起有重复、有已失效也有一些可能是别人反查出来的可疑域名。我需要把每个域名解析成IP然后判断哪些还能用哪些已经指向别处最后输出一张表格给团队做后续处理。刚开始我图省事直接在脚本里循环调用系统解析接口每个域名逐个处理。三千个域名看起来不多但实际跑起来才发现完全不是这么回事。有些域名解析很快几十毫秒就返回有些则要等两秒甚至更久才报超时还有些域名触发系统自动重试一连拖十几秒。统计下来平均每个域名要花将近一秒整批跑完四十多分钟算是运气好的。中途任何一次网络抖动脚本就卡在那里我得时不时盯着进度条那体验基本等于手动操作了。后来我意识到问题不在脚本本身而在工具选型。系统的域名解析接口是给普通应用用的设计目标是稳定、可靠、对资源友好而不是批量、高速、吞吞吐吐。我拿它跑三千个域名就像用家用轿车拉货虽然也能装但效率肯定不行。正确的做法是用一台皮卡——massdns和aiodns就是那类专门干活的工具。1.2 系统解析接口的三个隐藏瓶颈先说说系统解析接口慢的原因理解了这些才能明白后面为什么massdns能快这么多。第一个瓶颈是同步阻塞。普通循环调用解析接口时发出一条查询请求后要等响应返回才能处理下一个。网络往返需要时间哪怕只有几十毫秒三千个域名累加下来也是几十秒的纯等待。如果中间有几个域名超时每个超时等待几秒总时长直接飙升。这是最直观的瓶颈一次只发一个请求链路利用率极低。第二个瓶颈是系统解析库内部的复杂机制。现代操作系统的解析器为了稳定性会做很多额外工作比如多服务器故障转移、超时重试、缓存管理甚至还会对某些特殊情况做特殊处理。这些机制平时是优点但在批量场景下就变成了包袱。一个不可达的域名会让解析器连续尝试多个服务器每一轮都在白白消耗时间。我甚至在日志里见过某个已经失效的域名系统解析器花了几十秒才最终放弃原因是它在多个上游之间反复切换。第三个瓶颈容易被忽略每次解析都要新建连接、查询配置、读取本地解析文件。系统解析接口为了通用每次调用都会检查一遍设置这些固定开销加在几千次循环上损失非常可观。更别说如果解析器内部还有线程锁等并发控制大量请求挤在一起时反而会互相拖后腿。1.3 DNS查询的底层逻辑UDP无状态带来的并发红利要绕过前面这些瓶颈最简单有效的方法就是从DNS协议本身找突破口。标准的DNS基础查询走UDP端口一般是53。一条查询请求就是几十字节的报文发出去后服务器返回一条响应携带着解析结果。UDP是无状态的协议客户端发出请求后不需要维持连接、不需要握手响应到了能对上号就行。这本身就给并发留下了巨大的空间。既然无状态那我完全可以同时向服务器发送几百条甚至几千条查询等结果陆续返回再慢慢处理。响应报文里带着查询ID能一一对应上请求。传统阻塞式解析之所以慢不是网络不够快而是它把能一次完成的并发操作强行排成了串行。massdns这类工具做的事情本质上是把“顺序执行的服务员”换成“一批同时接待客人的服务员”同一批查询能在几秒内全部送出去然后集中等待结果回来。打个比方一个只有一个窗口的食堂每个窗口只有一个服务员一个学生打完饭下个学生才能继续几千个学生排队自然很慢。但如果食堂开了几百个窗口每个窗口还允许排几十个人服务员同时接待一批人出餐速度一下就上来了。massdns的设计核心就是后面这种思路用大量并发socket承载海量在途查询。2. massdnsC语言异步引擎的安装、参数实测与提速技巧2.1 安装与编译一次搞定massdns是个用C语言写的开源工具设计目标只有一个把DNS批量解析的速度压榨到极限。它不走系统解析库也不依赖第三方解析SDK而是直接从socket层面构造DNS报文、发送查询、接收响应。这意味着它对如何处理DNS报文的细节掌握得死死的速度上自然比普通脚本高一个量级。安装过程很简单。把官方仓库拉下来在项目根目录执行make就能在bin目录下生成可执行文件。这个工具依赖非常少在一台干净的最小化系统上也能顺利编译。编译完成后可以先用一条最简单的命令验证是否正常比如对一个你确定存在的域名发一条A记录查询。这一步能确认编译结果没问题同时也能顺带了解输出的基本格式。2.2 输入文件的正确姿势massdns的输入文件就是简单的文本每行一个域名不需要多余的空格或符号。它会按行读取逐条发送DNS查询。用户名文件里可以放普通的完整域名也可以配合外部脚本把字典前缀和目标域名组合成成百上千条候选域名再喂给它。我通常会把massdns的输入文件叫作query_list.txt里面内容长这样www.example.com mail.example.com api.example.com blog.example.com如果手里有一份通用子域名字典比如常见的api、mail、ftp、dev、test、stage又有一批目标主域可以用下面这个简单的命令生成组合列表while read prefix; do read main_domain 3 || break # 这里只是示意实际通常是双层循环 : done更常见的做法是用awk或Python一次性生成awk {for(i1;iNF;i) print $i} wordlist.txt prefixes.txt # 或者用Python这种“字典域名”的组合方式在早期资产收集中特别管用本质上是把一个个具体的子域名问题转化为“枚举可能存在的子域名并验证是否解析”massdns的高并发让它能在一个很短的时间里完成成千上万条候选的验证。2.3 参数调优每个选项在解决什么问题massdns的常用参数不多但每个都很关键。我直接写一条典型命令再逐个解释massdns -r resolvers.txt -t A -o S -w output.txt -s 8000 -c 100 --retry 1 query_list.txt各参数的含义和我的调优思路如下-r 指定DNS服务器列表文件每行一个服务器IP。服务器既可以是本地网络提供的解析服务也可以是公共DNS。建议至少准备两组以上方便分担压力。-t 指定查询类型A记录最常见需要查IPv6就用AAAA做邮箱资产那就是MX。-o S 指定输出格式为标准DNS文本格式每行是一段完整的DNS应答记录方便后续用awk处理。此外还有JSON格式选项适合直接进入程序流程。-w 指定输出文件把结果写进去避免刷屏。-s 控制同时打开的socket数量。socket相当于一个独立的网络端点数量越多容纳的在途查询就越多但系统可打开的socket数有限这个值太高会触发文件句柄限制。-c 控制每个socket上同时处理的查询数量。它和-s组成了整个“窗口”理论上在途查询总量约等于socket总数乘以单socket并发数我这组参数对应的理论窗口是80万实际受网络和上游服务器能力限制会自动调整。--retry 0或1即可UDP丢包是常态重试一次能挽回不少丢失的查询重试次数太多反而会让整体时间变长。参数调整最重要的原则是不要盲目追求数字。我试过一上来把-s拉到50000、-c拉到1000结果上游服务器直接拒绝服务大批查询返回超时或SERVFAIL吞吐反而下降。后来我意识到并发上限不是由客户端决定的而是由上游解析服务器能承受多少压力决定的。调参要小步试先跑一个小批次看错误率再往上加。2.4 实测结果速度对比我用同一个三千行的域名列表做过对比。系统脚本循环解析跑完用了四十多分钟massdns用上面那组参数大概十秒出头就完成了全量查询。当然十秒的成绩建立在大部分查询都能快速响应的基础上中间夹杂的几个超时域名会稍稍拖慢节奏但整体依然称得上“瞬间完成”。massdns的输出结果也很有特点。成功的解析会输出类似下面这样的行www.example.com. 3600 IN A 93.184.216.34 mail.example.com. 300 IN A 203.0.113.10这种格式保留了完整的DNS应答信息包括域名、TTL、记录类型和解析结果。我一般直接用awk把IP字段提取出来和原表对齐awk $4A {print $1, $5} output.txt massdns_ip.txt到了这一步粗筛工作基本完成哪些域名能解析、解析到哪个IP已经心里有数了。3. aiodns让批量解析嵌进Python工作流的正确姿势3.1 为什么还需要一个Python侧的解析器massdns再快也只是一个命令行工具。很多场景下我需要的不只是“跑出结果”而是要把解析动作嵌入到现有的数据处理流程里比如读取数据库里的域名列表、解析后直接写入台账、对结果做自定义判断。这时候单纯靠massdns的输出文件去对接会显得有点笨重尤其是当解析逻辑要和业务逻辑交错执行时。aiodns就是来解决这个问题的。简单来说它是基于asyncio的Python异步DNS解析库底层封装了异步解析引擎对上层提供await友好接口。你在Python代码里写一个async函数await查询某个域名它不会阻塞整个事件循环一批查询可以像排队叫号一样并发进行。从定位上讲massdns是一通到底的“挖掘机”适合一次性把大量域名筛一遍aiodns则是嵌入代码里的“灵活机械臂”适合在程序里反复调用配合业务逻辑做精细化处理。两者不是二选一而是前后衔接的上下游。3.2 aiodns的安装与坑安装很简单pip安装即可pip install aiodns它底层依赖一个C库在大多数Linux发行版和macOS上装完就能直接用。我在用的时候遇到过几个小坑写出来帮你绕过去。第一不要在每次查询时都新建一个解析器对象。实例化解析器本身有开销异步场景下更是如此正确做法是在程序启动时创建一个全局解析器反复使用。第二aiodns默认的超时和重试行为有时候太保守。跑批量任务时我习惯显式设置timeout和tries参数比如三秒超时、两次重试既能容忍网络抖动又不会让整个任务被卡住。第三Windows环境下异步事件循环的兼容性偶尔会有问题我的建议是生产环境优先在Linux或容器里跑避免花时间处理跨平台兼容问题。3.3 批量异步解析脚本的完整代码下面这个脚本是我在实际使用中打磨过的版本功能是读取一个域名文件异步并发解析A记录结果按域名和IP列表的对应关系保存成CSVimport asyncio import csv import aiodns resolver aiodns.DNSResolver(timeout3, tries2) async def resolve_one(sem, domain): async with sem: try: answers await resolver.query(domain, A) ips [item.host for item in answers] return domain, ips except aiodns.error.DNSError as exc: return domain, fERROR: {exc} async def main(input_file, output_file): with open(input_file, r, encodingutf-8) as f: domains [line.strip() for line in f if line.strip()] sem asyncio.Semaphore(300) # 控制并发数避免瞬时洪水 tasks [resolve_one(sem, domain) for domain in domains] results await asyncio.gather(*tasks) with open(output_file, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([domain, ip]) for domain, ip in results: writer.writerow([domain, ip]) if __name__ __main__: asyncio.run(main(domains.txt, result.csv))这段代码里有几个细节值得注意。第一是Semaphore的使用。起初我没有加限制三千个查询全部在同一时刻发出去结果系统连接表差点被打爆部分查询直接失败。加一个Semaphore把并发限制在三百左右整体稳定性明显改善。第二是asyncio.gather的使用它能把所有协程并发调度等待全部完成后统一收集结果比一个接一个await不知道省了多少时间。第三是错误处理解析失败不是一个可以忽略的情况我会把错误信息保留在结果中后续才能区分“无法解析”和“还没解析”。3.4 限流与错误处理并发不是越大越好用aiodns做批量解析时最容易把握不好的就是并发度。并发设太低异步的优势荡然无存并发设太高要么触发系统层面的限制要么把所有压力集中在某个上游解析服务器上导致大量超时。我的经验是并发数可以先设置为网络往返时间与单请求延迟的比值再乘一个余量但工程上并没有必要做这么细。更实际的办法是从小到大试先设100跑一小批看完成时间和错误率再逐步加到300、500。一个比较稳健的参考值是300500在多数普通网络环境下都能取得不错的吞吐表现。另外要留意的是错误类型不止超时一种。解析服务器可能返回SERVFAIL表示临时故障也可能返回NXDOMAIN表示域名确实不存在。aiodns抛出的异常里基本能区分出这些情况。我的脚本里暂时把所有错误都当成了一个标记但如果你要做更细的判断建议根据具体异常类型分别处理比如把SERVFAIL归为“重试”把NXDOMAIN归为“结果不存在”。4. 把两套工具拧成一条流水线从域名列表到结构化台账4.1 流水线设计massdns先粗筛aiodns再细查实际项目里我从来不会只用其中一个工具而是让它们各司其职形成一条流水线。第一步用massdns做粗筛。输入是完整的候选域名列表可能有一两万个域名其中大量是字典枚举出来的、不一定真实存在的。massdns在几十秒内跑完一遍输出结果的只有命中项天然把所有不存在的域名过滤掉。第二步用aiodns做细查。对massdns筛出来的命中域名再逐个补齐A、AAAA、CNAME、MX、TXT等记录把信息补全成一份真正的资产台账。这一步的数据量已经小了很多通常只有几百或几千条aiodns的灵活性完全够用。这种流水线的好处是层次清晰。massdns扛下最重的海量查询压力aiodns专注于业务逻辑需要的数据补全。两者各有分工互不抢活整套流程既快又稳。4.2 生成测试列表域名组合的两种实用姿势第二步的输入列表如果来源于“字典主域”的枚举那生成列表的方式可以分两种。一种是外部先把列表生成好再喂给massdns适合一次性的海量枚举任务。另一种是边解析边发现比如先用少量字典跑一遍拿到一轮结果后再根据业务特征生成第二批更精准的候选域名适合目标明确的场景。生成列表时我习惯保留一个“不可解析”的对照组。也就是说手动构造一批确定不存在的随机子域名混进列表里一起解析。这些随机域名如果也返回了解析结果那基本说明上游存在泛解析或某种特殊响应行为后续必须谨慎对待所有结果。4.3 格式化输出与去重统计跑完一轮后数据的沉淀形式很重要。massdns的结果是DNS文本行aiodns的输出是CSV。为了统一我会把massdns的结果也转成CSV再和aiodns的结果合并最后按IP网段、域名主体、记录类型做多维度归类。简单的处理脚本可以用Python完成核心逻辑是把域名作为主键IP列表作为值然后按IP网段聚合from collections import defaultdict import ipaddress ip_map defaultdict(set) with open(combined_result.csv, r) as f: for line in f: domain, ip line.strip().split(,) if not ip.startswith(ERROR): ip_map[ipaddress.ip_address(ip).version].add(ip) print(IPv4地址数:, len(ip_map.get(4, set()))) print(IPv6地址数:, len(ip_map.get(6, set())))这一类小工具代码每次都会有细节差异但本质上都是为了回答同一个问题这堆域名解析完之后它们到底指向了哪些网络资源。输出结构清晰后下一步无论是做资产盘点还是风险评估都会顺手很多。4.4 警惕泛解析当你以为发现了大量“活域名”时用批量解析做域名枚举时最容易踩的坑就是泛解析。有些主域名配置了泛解析任意不存在的子域名也会返回一个既定IP比如常见的*.example.com全部指向同一个负载均衡节点。这种情况下massdns会把所有枚举出来的“不存在的域名”都当成解析成功结果看起来收获满满实际上全是假数据。我在一次任务中吃过这个亏当时枚举出了一个非常大列表兴冲冲拿着结果去汇报结果抽查发现大量“命中”域名解析到的IP完全相同再一查原来全是泛解析。从那以后我加了一个固定的预处理步骤先用正常域名发起一条A记录查询记录返回的IP集合再跑批量解析结束后把所有解析到该集合的域名全部标记为“可能泛解析”交给人工确认。这一步在跨平台资产盘点时尤其重要否则你的台账里会充满并不存在的域名后续基于IP的统计全部失真。5. 高速解析的翻车现场限速、丢包和结果可信度5.1 限速问题的表象与应对高速解析第一个遇到的“翻车”就是上游限速。公共DNS服务为了保证服务质量对单IP来源的请求速率都有隐性限制。当我一次性把并发抬到上万时大量查询会直接超时massdns的统计里出现一大片TIME OUT完成时间反而比低并发时更久。应对限速有几个办法。第一是降低并发把窗口调回一个上游服务器能够承受的范围。第二是分散负载准备一个规模更大的解析服务器列表把查询均匀分散到多个上游。第三是本地缓存一层在解析服务器和massdns之间加一个本地DNS缓存服务让缓存命中一部分重复查询减轻上游压力。我实际使用中最多的是第二种因为这些工具本身就是为了批量而生的并发降到几千就足以在几十秒内完成海量查询。给每个上游服务器分担的请求量控制在可接受范围内大家都能稳定工作。5.2 丢包、重试与超时机制UDP最大的特点是快但代价是丢了不负责。网络环境一复杂丢包率就会上升尤其是大批量发送时某些查询可能在网络中悄悄消失客户端却在傻等。massdns的重试参数能缓解这个问题但设置重试次数时要记住一个平衡点重试能挽回丢失的查询但也会让总时长翻倍。实际使用中重试一次基本够用重试两次以上就有点得不偿失。aiodns场景下也一样合理的tries设置为2配合3秒左右的timeout基本上能在稳定性和速度之间取得很好的平衡。我在写脚本时还会特别处理一下“部分失败”的情况如果一个大任务中有少量查询失败不打断整个流程而是先把成功的结果固化下来再单独把失败项汇总到另一个文件等下一轮补充查询。这样即使网络抖动主流程也不会被拖垮。5.3 抽样交叉验证让数据站得住脚高速解析工具跑出来的结果配合快速但可信度需要额外验证。我的习惯是批量任务完成后随机抽出100条结果用系统自带的解析工具逐条交叉比对。比对时重点看IP是否一致、是否存在间歇性失败。如果发现系统解析跑出来的结果和massdns不一致差异率超过几个百分点那就要警惕是否并发太高导致上游服务器漏响应或返回了错误结果。交叉验证还有一个好处能在正式出成果前提前发现配置问题。比如ab畸形列表、输入的域名格式错误、服务器列表里混入了不可达地址等这些小问题如果不抽查很难从海量结果中一眼发现。每次做完批量解析我一定会花三五分钟做一次抽样宁可慢一点也要保证给到下游的数据是站得住脚的。5.4 后续扩展把解析结果和业务联动起来解析完成不代表任务结束。对我来说解析结果只是原材料怎么用才是关键。对运维场景来说解析结果可以反馈到监控系统检测某些域名的解析结果是否发生异常跳变对资产盘点场景来说解析结果可以和端口扫描、服务识别等结果联动形成一张更完整的资产画像。我做的最多的一类扩展是“周期性快照”。每周对同一批核心域名做一次批量解析把结果存档然后对比两次结果自动标出新增、消失和IP变更的域名。这套联动方案让DNS解析从一次性的被动工具变成了持续性的主动监控手段价值一下子就上来了。最后分享一个实际心得速度不是批量解析的唯一目标准确率和稳定性比快几秒宝贵得多。每次调大并发之前先问问自己面对这些结果的人能不能接受几个百分点的误差。如果答案是不能那就在速度和可信度之间找一个让你睡得好觉的平衡点。每次做完批量解析我也会习惯性地保留一份原始输出不急着清理因为谁也不知道下一次排查问题的时候这些“看着没用的原始数据”会不会就是唯一能还原现场的线索。