
做安全分析或者网络运维的人多半会遇到过这样一个场景某个域名前天还指向一台正常的Web服务器今天就变成了钓鱼页面一个看似人畜无害的子域名它历史解析过的某个IP却和恶意软件的基础设施共享同一台宿主。想弄清楚这些问题靠“现查一次”没用你要的是“历史记录”——过去30天、90天甚至一年里这个域名到底解析到过哪些IP。这就是被动DNS数据库在做的事。被动DNS说直白一点就是在旁边“偷听”真实世界里的DNS查询和应答把这些记录原封不动存下来形成一份会持续增长的域名解析历史库。它和主动扫描的区别在于主动扫描是你在某个时间点去问答案只代表那一刻被动DNS是月复一月沉淀下来的真实流量能反映出一个域名跟哪些IP绑过、什么时候变的、变化的频率有多快。对于安全事件追溯、僵尸网络研究、基础设施测绘来说这份历史数据几乎是不可替代的。而“自建”这套库又是另一个维度的需求。市面上商业被动DNS数据服务不少但企业内部域名、内网解析记录第三方平台根本采集不到把全量查询日志交给外部机构又牵扯数据主权和合规问题。自建的好处就是数据完全在自己手里、查询没有配额限制、想怎么加工都行。我自己在维护一套这样的系统规模不算大每天几千万条解析记录一台机器加PostgreSQL就能跑得很稳。这篇文章把原理和实现细节都梳理一遍适合安全运营同学和DNS基础设施维护者参考。1. 为什么要自建被动DNS数据库1.1 被动DNS解决什么核心问题被动DNS的本质是把“DNS解析”这个互联网中最基础的行为记录下来。每次用户访问一个网站、每个终端连接一台服务器几乎都离不开一次DNS查询。这些查询和应答经过递归DNS服务器和权威DNS服务器后会留下痕迹。被动DNS系统就是在这些痕迹被丢弃之前把它们复制一份、结构化、存起来。存下来之后至少能回答三类问题。第一类是“某个域名在指定时间段内解析到过哪些IP”这是域名基础设施变化分析的基础。第二类是反向的“某个IP被哪些域名解析过”这个查询对判断恶意IP的关联域名特别有效。第三类是“某个域名的解析频率和趋势”比如突然一天的查询量暴增几十倍往往意味着这个域名开始被大规模分发或传播。这三类问题在常规的日志系统里很难高效回答。公司日常的DNS日志通常会按天滚动、按文件归档想跨30天做一次关联查询手工翻日志基本不现实。而被动DNS数据库从一开始就按分析模型设计表结构和索引查询是秒级的。1.2 商业数据服务与自建的取舍商业被动DNS服务比如国内外几家威胁情报厂商提供的数据API数据覆盖度确实很好因为它们可能在多个大流量节点上有采集点。但实际使用中自建和商用两者并不是完全二选一而是互补关系。商用的优势是数据量大、覆盖面广能看到全球范围内的解析变化这些是自建做不到的。但商用服务有几个头疼的地方。一是接口配额安全事件突发时分析师频繁调用API很快就会触到速率限制。二是数据延迟部分服务的数据同步有数小时甚至一天的延迟用来做实时溯源会很憋屈。三是数据主权企业内部域名、预发布环境的域名、内网专属域名的解析记录第三方大概率没有也不会帮你采集。自建能弥补的正是这些短板。你可以在事件发生后的几秒钟内查到自己网络里的真实解析变化数据完全私有。当然自建的覆盖范围也有限只能覆盖你能够采集到流量的那部分DNS空间。所以比较合理的架构是自建为主、商用补充两者结合使用。1.3 自建这套系统需要什么基础条件先说结论一个小型安全团队完全可以搞定。我自己搭建这套系统时团队里只有我一个人在维护并没有专职的SRE。你需要具备的基础能力大致有三块。一是对Linux系统和网络抓包的基本操作能力因为采集端要部署在DNS服务器旁或流量汇聚点。二是数据库的基本功至少要会建表、写SQL、做分区。三是有一点脚本开发能力Python、Go都行用来写数据清洗、入库和查询接口。数据分析能力可以后面慢慢积累但这三类工程基础是起步的底线。如果你所在的组织DNS查询量极小每天几十万条那也可以用一台普通的虚拟机跑起来。如果每天上亿条那存储和采集架构要重新设计比如引入Kafka做缓冲、ClickHouse做存储但核心原理不会变。2. 采集层数据从哪来怎么来2.1 三种主流采集方式对比采集是整个被动DNS系统的第一环也是最容易被低估的一环。数据进不来后面所有存储和分析都是空谈。我整理一下三种主流方式的特点采集方式原理优点缺点适用规模DNS服务器日志直接读取BIND/CoreDNS/Unbound等服务器的日志输出实现最简单不额外引入抓包设备日志量大会拖累DNS服务器I/O有些日志格式信息不全中小规模SPAN端口镜像抓包交换机把DNS服务器端口的流量复制一份给采集机用dnscap等工具抓取不依赖DNS服务器厂商处理的是原始数据包遇到DoT/DoH加密流量直接失效SPAN流量大时采集机压力大中大规模dnstap协议DNS服务器通过structured protocol输出封装好的protobuf消息性能高时序准确能拿到握手和完整事务需要DNS服务器支持dnstapBIND 9.11Knot等有支持中大规模我在实际项目中用过日志方式和SPAN抓包方式。日志方式最快但要注意一个问题DNS服务器的日志为了保障正常运行通常会做轮转和限流如果查询量突然暴增部分日志可能还没来得及写就被丢弃。SPAN抓包的问题则在于加密DNS。现在很多客户端都在用DoT和DoH如果抽样端口上碰巧全是加密流量你能抓到的就只有密文根本解析不出域名。如果你所在组织有DNS加密的趋势我建议优先考虑dnstap方案它是在DNS服务器进程内部输出的即使加密也能在解密前拿到明文的查询内容。2.2 一个可落地的采集方案示例这里给出一套我实际验证过的采集方案使用BIND的dnstap能力。首先确认BIND版本。BIND 9.11及以上版本编译时带dnstap支持可以用named -V查看。如果编译参数里有--with-dnstap这一项就说明支持。然后在named.conf里启用dnstap指定要记录的事件类型dnstap { output file /var/log/dnstap/dns.capture.tap; version 1; query yes; response yes; }; logging { channel dnstap_log { syslog named; severity info; }; };启用之后BIND会不断把DNS事务写入到dns.capture.tap这个二进制文件。注意这个文件不是普通文本需要用dnstap相关的工具来读取。我习惯在采集机上部署一个消费管线每隔几秒就把新产生的tap文件片段转成JSONL再做后续入库。转换命令很简单dnstap-read dns.capture.tap dns-events.jsonl如果不想用dnstap-read直接抓包也有现成方案。在采集机的网卡上做镜像后用dnscap持续抓取dnscap -i eth0 -f udp port 53 or tcp port 53 -o /data/dns/ -w dns-capture-%Y%m%d%H%M%S.pcap抓下来的pcap文件再丢给后续解析程序处理。这里的核心经验是不要在采集机上做太重的实时解析把抓包和解析分成两层。抓包进程保持简单稳定解析进程可以随时改代码重启不会影响前面的数据包捕获。2.3 采集端的时钟、丢包与守护采集端有三个坑我逐一踩过提前说出来帮你们绕开。第一个是时钟同步。采集机上必须配置NTP或者chrony并且统一用UTC时间存数。我接手过一套历史系统老运维偷懒用本地时间UTC8存了一个月的时间戳等到你要把前后事件关联起来时所有时间都偏移了8小时而且期间日志已经轮转根本没法修复。最后只能重跑整条清洗管道教训惨重。第二个是静默丢包。抓包程序在流量峰值时如果处理不过来内核缓冲区溢出会直接丢弃数据包。表面上采集进程还在跑实际上数据已经缺了一大块。这种静默丢包非常危险因为事后你根本发现不了。解决方案是在采集机上监控网卡丢包计数定期告警同时尽量用专门的采集板卡或增大套接字缓冲区。第三个问题是守护进程的健壮性。采集端要跑在后台不能因为一个小异常就挂掉。用systemd托管、加Restartalways、配好日志轮转这些基础操作缺一不可。我之前用的是Python写的采集守护脚本后来觉得不稳换成了用systemd托管二进制工具加一个简单的shell循环稳定性反而提升了很多。3. 数据模型与选型决定查询效率的关键3.1 一条DNS记录应该存什么字段采集到的事件是一条一条的“查询请求——应答”对入库前至少要提取出这些字段字段名类型说明query_timetimestamptzDNS事务发生时间毫秒精度domaintext域名统一小写、去掉尾部的点record_typetextA、AAAA、CNAME、MX、TXT等response_codetextNOERROR、NXDOMAIN等应答码answerstext应答内容比如解析出来的IP列表client_ipinet发起查询的客户端IPserver_ipinet处理查询的DNS服务器IPclient_asnint客户端IP所属ASN编号可后续回填client_countrytext客户端所在国家/地区可后续回填这里有两个额外字段client_asn和client_country是我强烈建议一开始就预留的。后面做威胁情报分析时经常要按来源聚合比如“某个恶意域名被哪些ASN的机器高频请求”没有这两个字段就非常难做。但我对answers字段的建议是不要为了方便做规范化就把它拆成关联表。绝大多数分析查询只是想知道“这个域名解析到过哪些IP”直接把应答IP用逗号拼成字符串存到answers字段里就够了。只有当你明确要做全量IP反查时才需要单独设计解析映射表。3.2 PostgreSQL还是ClickHouse存储选型是整套系统里最关键的技术决策。我给的结论很简单数据量日均在5000万条以内PostgreSQL完全能扛住如果你确信未来会到亿级直接上ClickHouse。PostgreSQL最大的优势是SQL功能完整、运维生态成熟。你自己的团队如果本来就在用PostgreSQL那复用现有的备份、监控、高可用组件就可以学习成本为零。缺点是当数据量到一定程度时复杂的范围查询会变慢索引膨胀明显。ClickHouse的优势是列式存储带来的高压缩比和极其优秀的聚合性能配合TTL能自动清理过期数据特别适合这种时间序列型数据。缺点是需要专门运维对SQL的支持虽然越来越强但一些复杂JOIN会让新手抓狂。我的建议是不要一上来就上ClickHouse。被动DNS数据的特点是“写入量大、查询相对简单、按时间范围切片”PostgreSQL只要做好分区和索引跑个几千万条记录完全没问题。等真到了亿级再迁移也不迟原理和数据模型不变。3.3 表结构设计举例以PostgreSQL为例我的建表语句大致长这样CREATE TABLE dns_passive ( id BIGSERIAL, query_time TIMESTAMPTZ NOT NULL, domain TEXT NOT NULL, record_type TEXT NOT NULL DEFAULT A, response_code TEXT NOT NULL, answers TEXT NOT NULL, client_ip INET NOT NULL, server_ip INET NOT NULL, client_asn INTEGER, client_country TEXT ) PARTITION BY RANGE (query_time); CREATE INDEX idx_dns_passive_time ON dns_passive (query_time); CREATE INDEX idx_dns_passive_domain ON dns_passive (domain); CREATE INDEX idx_dns_passive_client_ip ON dns_passive (client_ip);这里有个细节值得说domain字段用TEXT而不是做归一化处理。因为域名很长而且分析查询往往就是精确匹配建B-tree索引就够了。如果你的查询经常带example.com这样的后缀匹配可以额外建一个domain_reverse字段把域名字符串反转便于后缀前缀匹配但我个人不做因为转储和清洗成本太高收益有限。创建分区时我用的方案是“按月建分区”。原因很简单绝大多数查询都是“最近30天”“最近90天”按月分区能让查询快速剪裁掉无关数据。分区表需要定期提前创建否则新月份的数据写入会直接报错。3.4 写入性能调优被动DNS数据的写入量和常规业务完全不是一个量级这里必须做批量写入。我实测过PostgreSQL逐条INSERT每天几百万条就很吃力了改成COPY或者多行INSERT批量提交后每天几千万条也能轻松处理。批量写入的实践建议是攒批、每5秒或者每攒够500行就flush一次。用Python做的话伪代码类似rows [] for event in stream_events(): rows.append(to_tuple(event)) if len(rows) 500: cursor.executemany(INSERT INTO dns_passive VALUES (...), rows) rows.clear()还有一点如果你要导历史数据我建议先把索引全部drop掉等数据导入完成后再重建索引。边写边建索引的时间比先导数据再统一建索引慢好几倍。这个优化在数据量大的时候效果非常明显。4. 查询层面向安全分析的SQL实战4.1 给定域名查看历史解析记录这是被动DNS系统最基础的用法。分析师拿到一个可疑域名后第一反应就是看它最近解析到过哪些IP。SQL可以这样写SELECT query_time, domain, record_type, answers FROM dns_passive WHERE domain suspicious.example.com AND query_time 2025-01-01 00:00:00 AND query_time 2025-03-01 00:00:00 ORDER BY query_time DESC LIMIT 200;这套查询的关键在于强制带时间范围。如果分析师忘记加时间条件数据库就会扫全部分区慢到不可接受。所以我在查询层永远会默认拼接一个“最近90天”的窗口并允许用户显式扩大。如果你还想快速看到“这个域名解析结果一共发生过多少次变化”可以稍微改一下SQL用DISTINCT ON取每个解析IP的最后出现时间。这类聚合查询对判断域名快速切换很有帮助。4.2 给定IP反向查询解析过它的域名反向查询在威胁回溯中更常用。已知一个恶意IP想知道它历史上为哪些域名提供过解析服务可以跨表查。如果你遵循了前面的建议把应答IP原样存在answers字段里那么反向查询就得用LIKE了。量小的时候可以凑合用量大了以后特别慢。真正的解法是单独维护一张解析映射表CREATE TABLE dns_resolution_map ( domain TEXT NOT NULL, ip INET NOT NULL, query_time TIMESTAMPTZ NOT NULL, PRIMARY KEY (domain, ip, query_time) ); CREATE INDEX idx_dns_resolution_ip ON dns_resolution_map (ip);这个表可以由入库任务同步填充也可以从dns_passive里定期跑一段ETL生成。有了这张表之后反向查询就变成SELECT domain, query_time FROM dns_resolution_map WHERE ip 192.0.2.123 AND query_time 2025-01-01 ORDER BY query_time DESC LIMIT 200;性能会比LIKE匹配好一个量级以上。4.3 时间趋势与数量聚合分析有时我们需要观察一个域名或一组域名的请求量随时间的变化趋势用于识别突发流量或失陷迹象。SQL如下SELECT date_trunc(hour, query_time) AS hour, count(*) AS query_count FROM dns_passive WHERE domain suspicious.example.com AND query_time 2025-01-01 00:00:00 AND query_time 2025-02-01 00:00:00 GROUP BY hour ORDER BY hour;执行完这条语句就可以把结果丢给可视化模块画一个折线图。某几个时间点的突然尖峰往往是安全事件的核心线索。如果查询目标是多个域名把domain IN (...)加进去就行。4.4 只读API封装我强烈建议不要直接给分析人员开放数据库账号。一方面权限不好控制另一方面一个不小心写错JOIN就可能导致数据库连接池被占满。更好的方案是封装一个只读API只暴露两个核心端点/api/resolve_history和/api/reverse_lookup。API内部实现很简单就是接收参数、校验、组装SQL、执行查询、返回JSON。用Flask之类轻量框架就够了核心是做好三件事一是有鉴权二是强制时间窗口三是限制返回行数。我在封装时还加了一个简单的缓存层对同一个域名的重复查询直接走内存缓存能把后端数据库的压力降下去一半。5. 自建过程中的坑与管理经验5.1 数据质量被扫描器和监控探针污染被动DNS采集到的数据并不都是正常业务流量。端口扫描器、漏洞扫描器、监控探针、安全巡检系统都会持续发起大量的DNS查询。这些探针流量会让某个域名的查询次数异常升高分析师的置信度会被带偏。我的过滤经验有三条。第一条是维护一个内部白名单把自家监控系统、NMS平台、DNS探测器的源IP排除掉。第二条是注意过滤arpa域的PTR查询这类反查请求量巨大分析价值低。第三条是对“同源同目标超高频查询”做聚合限流比如同一分钟内来自同一IP且对同一域名发起超过一定阈值次数的请求可以只保留第一条记录。5.2 去重的粒度怎么掌握DNS数据天然有大量重复。同一用户在一分钟内重复访问同一网站会产生多条记录同一个递归服务器向权威服务器发起的查询也可能被采集端抓成请求和应答两条。如果百分百去重需要加一个MD5指纹字段做唯一键这会显著增加存储和计算开销。我实际用去重粒度是“一分钟内、同一域名、同一记录类型、同一应答IP集合”合并为一条记录。这个粒度能大幅降低冗余同时不丢失时间信息保留这分钟内第一次和最后一次出现的时间。如果你处理的是威胁情报溯源场景这个粒度够用了。5.3 分区维护是容易遗漏的隐形故障PostgreSQL分区表是需要定期维护的。使用PARTITION BY RANGE后如果新月份到了却没提前建好分区写入直接报“no partition of relation found for row”错误。这个故障不是一开始就出现的通常是在某个跨月的深夜数据采集任务忽然开始批量失败。解决办法很简单——在crontab里写一个每月1号凌晨自动创建下月分区的脚本或者使用pg_partman来管理。我没有用插件直接写了一个shell脚本每月初跑一次把未来三个月的分区预建好几年下来没出过问题。5.4 数据保留策略被动DNS历史数据的特点是越旧的数据分析价值越低但又不舍得删。我的经验是把生命周期分三档数据类型保留时长说明原始明细90天高于90天的原始事件通常场景不到域名-IP聚合映射1年反向索引查询常用值得长期保留按日/按周聚合统计2年用于趋势和基线评估这个保留策略可以根据组织业务量调整。如果你的存储空间充足原始明细保留180天也行如果空间紧张先把聚合表保留好原始明细缩短到60天也未尝不可。5.5 查询性能优化和限流最后一个坑是查询性能失控。分析师写SQL时如果不自觉地带一个特别大的时间范围比如一次查三年的全量数据即使有分区也可能让数据库I/O长时间打满。我在API层加了一个硬性限制单次查询时间跨度最长90天超出就拒绝并提示分段查询。同时在数据库侧给分析账号设置语句超时statement_timeout设成15秒避免慢查询拖垮整个业务。6. 扩展从被动DNS到威胁情报工作流6.1 与威胁情报源联动被动DNS数据库本身记录的是“事实”并不区分“恶意”和“良性”。只有把外部威胁情报源的恶意域名表拿过来做关联才能让这份事实变成情报。实操时可以定期把外部的恶意域名列表导入一张threat_indicator表然后做关联查询SELECT d.domain, d.answers, t.source, t.threat_type FROM dns_passive d JOIN threat_indicator t ON d.domain t.domain WHERE d.query_time 2025-01-01;这样能直接看到哪些域名命中威胁情报、解析到哪些IP、被哪些客户端请求。这类关联分析也是日常威胁狩猎的主要工作流之一。6.2 与网络测绘联动被动DNS还能和主动扫描工具联动形成闭环。当被动DNS发现某个域名新解析到一个之前从未出现过的IP时会自动触发对那个IP的主动扫描把开放端口、服务版本、证书信息拉下来形成一个完整的观测链。我在工作中用这个思路发现过好几个疑似C2基础设施一个域名突然间解析到云上某个新IP主动扫描发现该IP只开放了443端口且服务器版本比较新结合后续流量分析基本可以确认是新建的钓鱼页面。6.3 可视化查询页面最后一步才是可视化。整个体系跑通之后安全团队需要的是一个能上手用的前端页面。不需要特别复杂输入域名显示时间线输入IP显示反查列表再加上热度趋势就足够了。我自己用的是一个简单的Web页面套上后端API没有用太重的前端框架。接入公司统一登录体系之后安全分析师每天都能在上百个域名上做批量检索。让工具被团队真正用起来这个项目才算闭环。算下来这套系统我前后维护了快三年最大的体会是被动DNS难的不是某个单一环节难点在于把采集、存储、查询、情报联动串成一条稳定运转的流水线。数据采集隔三差五会出幺蛾子存储层的容量规划要提前做查询接口的设计要耐住性子磨。但等到某个安全事件发生时你能在一分钟内调出某个域名过去90天的全部解析变化那种踏实感是其他数据源给不了的。如果你也在考虑自建一套别犹豫先跑起来再说。