Vector dnstap Source 深度实战:从 BIND 采集 DNSTAP DNS 查询日志的完整配置与解析机制
【免费下载链接】vectorA high-performance observability data pipeline.项目地址: https://gitcode.com/GitHub_Trending/vect/vector
本篇指南以 Vector 的dnstapsource 组件为核心,讲解如何配置 TCP/Unix Socket 两种监听模式从支持 DNSTAP 协议的 DNS 服务器(以 BIND 为例)采集 DNS 查询与动态更新日志,覆盖全部配置参数、输出事件字段结构、可运行的实操示例,并深入到 src/sources/dnstap/mod.rs 的帧处理与解析实现。读完你可以独立完成 DNS 服务器与 Vector 的对接,理解rawData解析链路和内部遥测事件的含义。
组件定位与核心属性
dnstapsource 用于从 dnstap)。它监听一条 socket 连接,接收 DNS 服务器(如 BIND)发出的 DNSTAP 帧(Protobuf 编码),将其解析为结构化的日志事件向下游输出。
组件元数据定义在 website/cue/reference/components/sources/dnstap.cue,关键属性如下:
| 属性 | 值 | 说明 |
|---|---|---|
| 组件类型 | source | 数据入口 |
| 交付语义(delivery) | best_effort | 尽力而为,不保证每条事件都送达 |
| 确认机制(acknowledgements) | false | 不支持端到端确认(源码中can_acknowledge()返回false) |
| 部署角色 | daemon | 作为守护进程运行 |
| 开发状态 | beta | 仍处于 beta 阶段 |
| 有状态 | false | 无状态组件 |
| 输出数据 | logs | 输出日志事件,不支持 multiline |
由于不支持确认且是尽力交付,生产环境建议将其用于可容忍少量丢失的 DNS 可观测性场景(查询分析、异常检测等),而非强一致性场景。
配置结构总览
配置结构由 src/sources/dnstap/mod.rs 中的DnstapConfig定义:顶层公共参数之外,通过mode字段(serde tag)在Tcp(TcpConfig)与Unix(UnixConfig)两个变体之间切换。TcpConfig与UnixConfig分别定义在 src/sources/dnstap/tcp.rs 和 src/sources/dnstap/unix.rs。参数说明数据(类型、默认值、适用条件、示例值)由 website/cue/reference/components/sources/generated/dnstap.cue 生成。
公共参数(tcp / unix 两种模式通用)
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
mode | string (tcp|unix) | 是 | unix 平台默认 unix,非 unix 平台默认 tcp(0.0.0.0:9000) | 使用的 dnstap socket 类型 |
max_frame_length | uint(字节) | 否 | 102400(100 KiB,见default_max_frame_length()) | 源接受的最大 DNSTAP 帧长度,超过的帧直接丢弃 |
host_key | string | 否 | 全局log_schema.host_key | 覆盖写入对端地址的日志字段名,值为 socket 地址本身 |
raw_data_only | bool | 否 | false | 是否跳过 DNSTAP 帧的解析/解码。为true时原始帧数据以 base64 字符串写入事件的rawData字段 |
multithreaded | bool | 否 | false | 是否并发处理 DNSTAP 帧 |
max_frame_handling_tasks | uint | 否 | 1000(源码中unwrap_or(1000)) | 可并发处理的最大帧数 |
lowercase_hostnames | bool | 否 | false | 是否将所有接收到的 DNSTAP 主机名统一转为小写,保证一致性 |
TCP 模式(mode = "tcp")
适用于跨主机接收 DNSTAP 数据,DNS 服务器通过网络连接到 Vector。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
address | string | 是 | - | 监听地址,必须包含端口;支持systemd{#N}使用 systemd socket activation 传递的第 N 个 socket。示例:0.0.0.0:9000、systemd、systemd#3 |
port_key | string | 否 | "port" | 覆盖写入对端端口的日志字段名,值形如9000;设为""可抑制该字段 |
permit_origin | string[] | 否 | - | 允许的源 IP 网段白名单,CIDR 表示。示例:192.168.0.0/16、127.0.0.1/32、::1/128 |
receive_buffer_bytes | uint(字节) | 否 | - | 每条连接使用的接收缓冲区大小 |
max_connection_duration_secs | uint(秒) | 否 | - | 单条连接最长保持时间,超时的连接会被关闭,有助于对长连接做负载均衡 |
connection_limit | uint(连接数) | 否 | - | 任意时刻允许的最大 TCP 连接数 |
keepalive | 对象 | 否 | - | TCP keepalive 设置(TcpKeepaliveConfig) |
shutdown_timeout_secs | uint(秒) | 否 | 30 | 关闭阶段强制关闭连接前的超时时间 |
tls | 对象 | 否 | 关闭 | TlsSourceConfig,开启 TLS 并支持从客户端证书中提取元数据 |
TCP 模式的一个关键细节来自 src/sources/dnstap/tcp.rs:当tls配置中指定了client_metadata_key时,DnstapFrameHandler::insert_tls_client_metadata会把客户端证书的subject写入事件的tls_client_metadata字段(键名可自定义),用于审计“哪个客户端连接在发送 DNSTAP 数据”。同时每个接收到的帧会触发SocketEventsReceived内部事件(mode = "tcp"),用于字节计数遥测。permit_origin在实现中映射为IpAllowlistConfig,最终转换为Vec<IpNet>网段表,不匹配的源地址连接会被拒绝。
Unix Socket 模式(mode = "unix")
适用于 Vector 与 DNS 服务器部署在同一台机器的最常见场景。DNS 服务器把 DNSTAP 数据写入 Vector 创建的 server UDS。
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
socket_path | string | 是 | /run/bind/dnstap.sock(源码UnixConfig::default()) | 读取 DNSTAP 数据的 socket 文件绝对路径;源首次启动时若不存在会自动创建 |
socket_file_mode | uint | 否 | 由umask决定 | socket 文件的权限位。支持任意数字格式,最直观的是八进制,如0o777、0o754、508(即0o774) |
socket_receive_buffer_size | uint(字节) | 否 | - | 接收缓冲区大小。注意:需要相应调整系统级最大 socket 接收缓冲区(Linux 的/proc/sys/net/core/rmem_max)才能生效 |
socket_send_buffer_size | uint(字节) | 否 | - | 发送缓冲区大小。注意:需要相应调整/proc/sys/net/core/wmem_max |
关于 UDS 的工作机制(源自 dnstap.cue 的 how_it_works 章节):
- 启动时,
dnstap源在指定路径创建新的 server UDS;若该路径上的 UDS 已存在(被占用),Vector 会先自动删除再创建。 - UDS 默认权限取决于当前
umask。为了让本机 BIND 能够向 UDS 写数据,需通过socket_file_mode显式设置权限,例如:
[sources.my_dnstap_source] type = "dnstap" mode = "unix" socket_file_mode = 0o774 # 其他配置- 使用远端 BIND 服务器:UDS 只能创建在本地机器,但可以配合 SSH 端口/通道转发把本地 UDS 转发到远端 BIND 所在主机,让远端 BIND 写入“本地”socket。确保两端 Unix socket 权限设置正确。
- 调整 UDS 缓冲区:在高负载场景下可平滑处理 DNS 流量尖峰,将
socket_receive_buffer_size/socket_send_buffer_size调到例如 10 MiB:
[sources.my_dnstap_source] type = "dnstap" mode = "unix" socket_receive_buffer_size = 10_485_760 socket_send_buffer_size = 10_485_760 # 其他配置事件输出字段结构
解析成功后,每个 DNSTAP 帧输出为一个日志事件。字段定义在 dnstap.cue 的 output 章节,并对应 src/sources/dnstap/mod.rs 中DnstapConfig::schema_definition引用的DnstapEventSchema(来自 lib/vector-vrl/dnstap-parser 的 schema 模块)。
顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
dataType | string | 否 | DNSTAP 事件数据类型,目前仅定义Message(载荷为 dnstap message) |
dataTypeId | uint | 是 | 数据类型数字 ID,如1 |
messageType | string | 否 | DNSTAP 消息类型(仅dataType = Message时有意义),枚举见下表 |
messageTypeId | uint | 是 | 消息类型数字 ID,如6 |
time | uint | 是 | DNS 消息发送/接收时间,为距 UNIX 纪元的timePrecision时间单位数 |
timePrecision | string | 是 | time的时间精度:s/ms/us/ns |
timestamp | string | 是 | 与time相同时刻,ISO 8601 UTC 字符串,如2021-04-09T15:08:32.767098Z |
serverId | string | 否 | DNS 服务器标识,如ns1.example.com |
serverVersion | string | 否 | DNS 服务器版本,如BIND 9.16.8 |
extraInfo | string | 否 | 事件的附加数据(任意字节注解的 base64 形式) |
socketFamily | string | 是 | INET(IPv4)或INET6(IPv6),决定如何解释地址字段 |
socketProtocol | string | 是 | UDP或TCP,决定如何解释端口字段 |
sourceAddress/sourcePort | string / uint | 是 / 否 | 消息发起方的网络地址 / 传输端口 |
responseAddress/responsePort | string / uint | 是 / 否 | 消息响应方的网络地址 / 传输端口 |
error | string | 否 | 解析 dnstap 数据失败时的错误信息 |
rawData | string | 否 | 解析失败或开启raw_data_only时出现的原始 DNSTAP 二进制数据 base64 |
requestData | object | 否 | DNS 查询/更新的请求消息数据,子结构见下 |
responseData | object | 否 | DNS 查询/更新的响应消息数据,子结构见下 |
messageType 枚举(14 种视角组合)
| 枚举值 | 语义(以谁为视角) |
|---|---|
AuthQuery/AuthResponse | 权威服务器收到解析器的查询 / 权威服务器发回响应 |
ResolverQuery/ResolverResponse | 解析器发往权威服务器的查询(通常清 RD 位)/ 解析器收到的响应 |
ClientQuery/ClientResponse | DNS 服务器视角:客户端发来递归期望的查询 / 服务器回给客户端的响应(通常置 RA 位) |
ForwarderQuery/ForwarderResponse | 下游服务器发往上游递归服务器的查询 / 上游回给下游的响应 |
StubQuery/StubResponse | 桩解析器视角的查询 / 响应 |
ToolQuery/ToolResponse | DNS 工具视角的查询 / 响应 |
UpdateQuery/UpdateResponse | 权威服务器视角收到的动态更新 / 发出的更新响应 |
requestData / responseData 子结构
- 公共字段:
time、timePrecision、fullRcode(4 位 header rcode + 8 位 opt extendedRcode 之和)、rcodeName(文本化响应码)、rawData(仅解析失败时出现,base64)。 rcodeName枚举覆盖 DNS 标准响应码:NoError、FormErr、ServFail、NXDomain、NotImp、Refused、YXDomain、YXRRSet、NXRRSet、NotAuth、NotZone,以及 EDNS 扩展码BADVERS、BADSIG、BADKEY、BADTIME、BADMODE、BADNAME、BADALG、BADTRUNC、BADCOOKIE。requestData特有子段:header、question、additional、opt(EDNS 伪段,含do、ednsVersion、extendedRcode、options、udpPayloadSize)、zone/prerequisite/update(DNS 动态更新三段,见 RFC 2136)。responseData特有子段:header、question、answers、authority、additional、opt、zone。其中opt还包含ede数组(扩展 DNS 错误,含infoCode、purpose、extraText,见 RFC 8914)。- 资源记录条目字段示例:
class、domainName、rData、recordType、recordTypeId、ttl。
实操示例一:采集常规 DNS 查询与响应
前置条件(来自 website/cue/reference/services/dnstap_data.cue 的设置说明):
- 参考 ISC 官方 KB 文章 “Using DNSTAP with BIND” 配置 BIND,使其把 DNSTAP 数据写入 Vector 将创建的 Unix socket;
- 确保该 socket 对 DNS 服务器进程可写(例如 BIND 对 socket 有
rw权限,可用socket_file_mode = 508即0o774实现); - BIND 与 Vector 两端配置的 Unix socket 路径一致。
Vector 配置:
[sources.my_dnstap_source] type = "dnstap" mode = "unix" socket_path = "/run/bind/dnstap.sock" socket_file_mode = 508 max_frame_length = 102400 max_frame_handling_tasks = 10000在 BIND 上执行一条本地查询:
nslookup host.example.com localhost源将输出两个事件。查询事件(节选):
{ "dataType": "Message", "dataTypeId": 1, "messageType": "ClientQuery", "messageTypeId": 5, "requestData": { "fullRcode": 0, "header": { "aa": false, "ad": false, "anCount": 0, "arCount": 0, "cd": false, "id": 49653, "nsCount": 0, "opcode": 0, "qdCount": 1, "qr": 0, "ra": false, "rcode": 0, "rd": true, "tc": false }, "question": [ { "class": "IN", "domainName": "host.example.com.", "questionType": "A", "questionTypeId": 1 } ], "rcodeName": "NoError", "time": 1614781642516276825, "timePrecision": "ns" }, "responseAddress": "127.0.0.1", "responsePort": 0, "serverId": "ns1.example.com", "serverVersion": "BIND 9.16.8", "socketFamily": "INET", "socketProtocol": "UDP", "sourceAddress": "127.0.0.1", "sourcePort": 52398, "time": 1614781642516276825, "timePrecision": "ns" }响应事件(节选):
{ "dataType": "Message", "dataTypeId": 1, "messageType": "ClientResponse", "messageTypeId": 6, "responseData": { "answers": [ { "class": "IN", "domainName": "host.example.com.", "rData": "192.0.2.100", "recordType": "A", "recordTypeId": 1, "ttl": 3600 } ], "authority": [ { "class": "IN", "domainName": "example.com.", "rData": "ns1.example.com.", "recordType": "NS", "recordTypeId": 2, "ttl": 86400 } ], "fullRcode": 0, "header": { "aa": true, "anCount": 1, "nsCount": 1, "qdCount": 1, "qr": 1, "ra": true, "rd": true, "id": 49653, "rcode": 0 }, "question": [ { "class": "IN", "domainName": "host.example.com.", "questionType": "A", "questionTypeId": 1 } ], "rcodeName": "NoError", "time": 1614781642516276825, "timePrecision": "ns" }, "responseAddress": "127.0.0.1", "responsePort": 0, "serverId": "ns1.example.com", "serverVersion": "BIND 9.16.8", "socketFamily": "INET", "socketProtocol": "UDP", "sourceAddress": "127.0.0.1", "sourcePort": 52398, "time": 1614781642516276825, "timePrecision": "ns" }注意事项:BIND 需托管example.com区域且区域内含host.example.com主机记录;BIND 与 Vector 的 Unix socket 路径必须一致;BIND 对 socket 需有rw权限。
实操示例二:采集 DNS 动态更新(Dynamic Update)
若 DNS 流量较大,可把 UDS 缓冲区调大(注意同步调整系统级rmem_max/wmem_max):
[sources.my_dnstap_source] type = "dnstap" mode = "unix" socket_path = "/run/bind/dnstap.sock" socket_file_mode = 508 socket_receive_buffer_size = 10485760 socket_send_buffer_size = 10485760对允许动态更新的权威 BIND 发送一条更新:
nsupdate <<EOF server localhost update add h1.example.com 3600 a 192.0.2.110 send EOF源输出UpdateQuery(messageTypeId: 13)与UpdateResponse(messageTypeId: 14)两个事件。UpdateQuery的requestData包含opcode: 5、upCount: 1、zoCount: 1的更新 header,zone段(zName: example.com.、zType: SOA)以及update段(domainName: h1.example.com.、rData: 192.0.2.110、ttl: 3600);UpdateResponse的responseData中qr: 1表示这是一条响应。注意事项与示例一相同,另需example.com区域允许动态更新。
源码级实现解析
帧处理主流程(mod.rs)
DnstapConfig::build(src/sources/dnstap/mod.rs)按mode分发:TCP 走build_framestream_tcp_source(附带MaybeTlsSettings),Unix 走build_framestream_unix_source(均来自 src/sources/util 的 framestream 工具)。核心帧处理逻辑集中在CommonFrameHandler:
content_type固定为"protobuf:dnstap.Dnstap",framestream 层据此做 protobuf 拆帧;handle_event中先发出BytesReceived(协议标记为 protobuf)内部事件做字节计数,再写入host(由host_key控制)等元数据;- 若
raw_data_only为true:整帧 base64 编码后写入rawData字段,不做任何解码; - 否则调用
DnstapParser::parse(来自 lib/vector-vrl/dnstap-parser),并把DnsParserOptions { lowercase_hostnames }传入以支持统一小写主机名;解析失败时发出DnstapParseError内部事件并丢弃该帧(返回None),不会生成部分事件; - 最后按日志命名空间(legacy / vector)写入
ingest_timestamp与source_type = "dnstap"等标准源元数据。
解析失败的可观测性
解析错误事件定义在 src/internal_events/dnstap.rs:以error_type = PARSER_FAILED、stage = PROCESSING记录日志,并递增component_errors_total计数器。监控该计数器即可发现上游数据格式问题。配合事件中的error与rawData字段(解析失败时保留原始 base64 数据),可以事后离线重放排查。
集成测试的验证方式
mod.rs末尾的integration_tests模块(需dnstap-integration-testsfeature)演示了端到端验证方式:在 Docker 中运行 BIND,通过rndc dnstap -reopen让 BIND 重新打开 DNSTAP socket(路径如dnstap.sock2),再用nslookup/nsupdate触发真实查询与更新,最终断言输出事件中同时存在ClientQuery/ClientResponse(或UpdateQuery/UpdateResponse/AuthQuery/AuthResponse)以及requestData.question[0].domainName、responseData.answers[0].rData等字段值正确。raw_data_only变体则断言所有事件都携带rawData字段。这套流程可以直接作为你自部署时的验收脚本参考。
相关能力:VRL 侧的 parse_dnstap 函数
仓库中 lib/vector-vrl/dnstap-parser/src/vrl_functions/parse_dnstap.rs 基于同一dnstap-parser库提供了 VRL 函数parse_dnstap(生成文档见 docs/generated/parse_dnstap.json)。若你在 source 层使用raw_data_only = true只落地原始 base64 数据,也可以后续在remap变换里用该函数按需解码,实现“先采集、后解析”的弹性方案。
使用注意事项小结
- 该组件交付语义为 best effort、不支持端到端确认(
can_acknowledge() == false),且开发状态为 beta,升级 Vector 版本时应关注其行为变化; mode = "tcp"时address必须带端口;跨机器部署建议配合permit_origin限制来源网段,高连接数场景用connection_limit与max_connection_duration_secs控制长连接;max_frame_length过小会静默丢弃大帧,DNS over TCP 的大报文场景下需要评估并调大;- Unix 模式的
socket_file_mode、socket_receive/send_buffer_size与系统 umask、/proc/sys/net/core/{rmem,wmem}_max存在联动关系,调大缓冲区前请先确认系统上限; - 事件字段完整 schema 与枚举说明以 website/cue/reference/components/sources/dnstap.cue 和 generated/dnstap.cue 为准,下游管道编写 VRL 查询时可据此构造精确的字段路径。
【免费下载链接】vectorA high-performance observability data pipeline.项目地址: https://gitcode.com/GitHub_Trending/vect/vector
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考