1use std::sync::LazyLock;
2
3use base64::prelude::{BASE64_STANDARD, Engine as _};
4use dnsmsg_parser::dns_message_parser::DnsParserOptions;
5use vector_core::event::LogEvent;
6use vrl::prelude::*;
7
8use crate::{parser::DnstapParser, schema::DnstapEventSchema};
9
10static DEFAULT_LOWERCASE_HOSTNAMES: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
11
12static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
13 vec![
14 Parameter::required(
15 "value",
16 kind::BYTES,
17 "The base64 encoded representation of the DNSTAP data to parse.",
18 ),
19 Parameter::optional(
20 "lowercase_hostnames",
21 kind::BOOLEAN,
22 "Whether to turn all hostnames found in resulting data lowercase, for consistency.",
23 )
24 .default(&DEFAULT_LOWERCASE_HOSTNAMES),
25 ]
26});
27
28#[derive(Clone, Copy, Debug)]
29pub struct ParseDnstap;
30
31impl Function for ParseDnstap {
32 fn identifier(&self) -> &'static str {
33 "parse_dnstap"
34 }
35
36 fn usage(&self) -> &'static str {
37 "Parses the `value` as base64 encoded DNSTAP data."
38 }
39
40 fn internal_failure_reasons(&self) -> &'static [&'static str] {
41 &[
42 "`value` is not a valid base64 encoded string.",
43 "dnstap parsing failed for `value`",
44 ]
45 }
46
47 fn category(&self) -> &'static str {
48 Category::Parse.as_ref()
49 }
50
51 fn return_kind(&self) -> u16 {
52 kind::OBJECT
53 }
54
55 fn parameters(&self) -> &'static [Parameter] {
56 &PARAMETERS
57 }
58
59 fn examples(&self) -> &'static [Example] {
60 &[example!(
61 title: "Parse dnstap query message",
62 source: r#"parse_dnstap!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zGgBy5wEIAxACGAEiEAAAAAAAAAAAAAAAAAAAAAAqECABBQJwlAAAAAAAAAAAADAw8+0CODVA7+zq9wVNMU3WNlI2kwIAAAABAAAAAAABCWZhY2Vib29rMQNjb20AAAEAAQAAKQIAAACAAAAMAAoACOxjCAG9zVgzWgUDY29tAGAAbQAAAAByZLM4AAAAAQAAAAAAAQJoNQdleGFtcGxlA2NvbQAABgABAAApBNABAUAAADkADwA1AAlubyBTRVAgbWF0Y2hpbmcgdGhlIERTIGZvdW5kIGZvciBkbnNzZWMtZmFpbGVkLm9yZy54AQ==")"#,
63 result: Ok(indoc!(
64 r#"{
65 "dataType": "Message",
66 "dataTypeId": 1,
67 "extraInfo": "",
68 "messageType": "ResolverQuery",
69 "messageTypeId": 3,
70 "queryZone": "com.",
71 "requestData": {
72 "fullRcode": 0,
73 "header": {
74 "aa": false,
75 "ad": false,
76 "anCount": 0,
77 "arCount": 1,
78 "cd": false,
79 "id": 37634,
80 "nsCount": 0,
81 "opcode": 0,
82 "qdCount": 1,
83 "qr": 0,
84 "ra": false,
85 "rcode": 0,
86 "rd": false,
87 "tc": false
88 },
89 "opt": {
90 "do": true,
91 "ednsVersion": 0,
92 "extendedRcode": 0,
93 "options": [
94 {
95 "optCode": 10,
96 "optName": "Cookie",
97 "optValue": "7GMIAb3NWDM="
98 }
99 ],
100 "udpPayloadSize": 512
101 },
102 "question": [
103 {
104 "class": "IN",
105 "domainName": "facebook1.com.",
106 "questionType": "A",
107 "questionTypeId": 1
108 }
109 ],
110 "rcodeName": "NoError"
111 },
112 "responseData": {
113 "fullRcode": 16,
114 "header": {
115 "aa": false,
116 "ad": false,
117 "anCount": 0,
118 "arCount": 1,
119 "cd": false,
120 "id": 45880,
121 "nsCount": 0,
122 "opcode": 0,
123 "qdCount": 1,
124 "qr": 0,
125 "ra": false,
126 "rcode": 16,
127 "rd": false,
128 "tc": false
129 },
130 "opt": {
131 "do": false,
132 "ednsVersion": 1,
133 "extendedRcode": 1,
134 "ede": [
135 {
136 "extraText": "no SEP matching the DS found for dnssec-failed.org.",
137 "infoCode": 9,
138 "purpose": "DNSKEY Missing"
139 }
140 ],
141 "udpPayloadSize": 1232
142 },
143 "question": [
144 {
145 "class": "IN",
146 "domainName": "h5.example.com.",
147 "questionType": "SOA",
148 "questionTypeId": 6
149 }
150 ],
151 "rcodeName": "BADVERS"
152 },
153 "responseAddress": "2001:502:7094::30",
154 "responsePort": 53,
155 "serverId": "james-Virtual-Machine",
156 "serverVersion": "BIND 9.16.3",
157 "socketFamily": "INET6",
158 "socketProtocol": "UDP",
159 "sourceAddress": "::",
160 "sourcePort": 46835,
161 "time": 1593489007920014129,
162 "timePrecision": "ns",
163 "timestamp": "2020-06-30T03:50:07.920014129Z"
164 }"#
165 )),
166 )]
167 }
168
169 fn compile(
170 &self,
171 _state: &TypeState,
172 _ctx: &mut FunctionCompileContext,
173 arguments: ArgumentList,
174 ) -> Compiled {
175 let value = arguments.required("value");
176 let lowercase_hostnames = arguments.optional("lowercase_hostnames");
177 Ok(ParseDnstapFn {
178 value,
179 lowercase_hostnames,
180 }
181 .as_expr())
182 }
183}
184
185#[derive(Debug, Clone)]
186struct ParseDnstapFn {
187 value: Box<dyn Expression>,
188 lowercase_hostnames: Option<Box<dyn Expression>>,
189}
190
191impl FunctionExpression for ParseDnstapFn {
192 fn resolve(&self, ctx: &mut Context<'_>) -> Resolved {
193 let value = self.value.resolve(ctx)?;
194 let input = value.try_bytes_utf8_lossy()?;
195
196 let mut event = LogEvent::default();
197
198 DnstapParser::parse(
199 &mut event,
200 BASE64_STANDARD
201 .decode(input.as_bytes())
202 .map_err(|_| format!("{input} is not a valid base64 encoded string"))?
203 .into(),
204 DnsParserOptions {
205 lowercase_hostnames: self
206 .lowercase_hostnames
207 .map_resolve_with_default(ctx, || DEFAULT_LOWERCASE_HOSTNAMES.clone())?
208 .try_boolean()?,
209 },
210 )
211 .map_err(|e| format!("dnstap parsing failed for {input}: {e}"))?;
212
213 Ok(event.value().clone())
214 }
215
216 fn type_def(&self, _: &TypeState) -> TypeDef {
217 TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use chrono::{DateTime, TimeZone, Utc};
224 use vrl::value;
225
226 use super::*;
227
228 test_function![
229 parse_dnstap => ParseDnstap;
230
231 query {
232 args: func_args![value: value!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zGgBy5wEIAxACGAEiEAAAAAAAAAAAAAAAAAAAAAAqECABBQJwlAAAAAAAAAAAADAw8+0CODVA7+zq9wVNMU3WNlI2kwIAAAABAAAAAAABCWZhY2Vib29rMQNjb20AAAEAAQAAKQIAAACAAAAMAAoACOxjCAG9zVgzWgUDY29tAGAAbQAAAAByZLM4AAAAAQAAAAAAAQJoNQdleGFtcGxlA2NvbQAABgABAAApBNABAUAAADkADwA1AAlubyBTRVAgbWF0Y2hpbmcgdGhlIERTIGZvdW5kIGZvciBkbnNzZWMtZmFpbGVkLm9yZy54AQ==")],
233 want: Ok({
234 let timestamp = Value::Timestamp(
235 Utc.from_utc_datetime(
236 &DateTime::parse_from_rfc3339("2020-06-30T03:50:07.920014129Z")
237 .unwrap()
238 .naive_utc(),
239 ),
240 );
241 value!({
242 dataType: "Message",
243 dataTypeId: 1,
244 extraInfo: "",
245 messageType: "ResolverQuery",
246 messageTypeId: 3,
247 queryZone: "com.",
248 requestData: {
249 fullRcode: 0,
250 header: {
251 aa: false,
252 ad: false,
253 anCount: 0,
254 arCount: 1,
255 cd: false,
256 id: 37634,
257 nsCount: 0,
258 opcode: 0,
259 qdCount: 1,
260 qr: 0,
261 ra: false,
262 rcode: 0,
263 rd: false,
264 tc: false,
265 },
266 opt: {
267 do: true,
268 ednsVersion: 0,
269 extendedRcode: 0,
270 options: [
271 {
272 optCode: 10,
273 optName: "Cookie",
274 optValue: "7GMIAb3NWDM=",
275 }
276 ],
277 udpPayloadSize: 512,
278 },
279 question: [
280 {
281 class: "IN",
282 domainName: "facebook1.com.",
283 questionType: "A",
284 questionTypeId: 1,
285 }
286 ],
287 rcodeName: "NoError",
288 },
289 responseData: {
290 fullRcode: 16,
291 header: {
292 aa: false,
293 ad: false,
294 anCount: 0,
295 arCount: 1,
296 cd: false,
297 id: 45880,
298 nsCount: 0,
299 opcode: 0,
300 qdCount: 1,
301 qr: 0,
302 ra: false,
303 rcode: 16,
304 rd: false,
305 tc: false,
306 },
307 opt: {
308 do: false,
309 ede: [
310 {
311 extraText: "no SEP matching the DS found for dnssec-failed.org.",
312 infoCode: 9,
313 purpose: "DNSKEY Missing",
314 }
315 ],
316 ednsVersion: 1,
317 extendedRcode: 1,
318 udpPayloadSize: 1232,
319 },
320 question: [
321 {
322 class: "IN",
323 domainName: "h5.example.com.",
324 questionType: "SOA",
325 questionTypeId: 6,
326 }
327 ],
328 rcodeName: "BADVERS",
329 },
330 responseAddress: "2001:502:7094::30",
331 responsePort: 53,
332 serverId: "james-Virtual-Machine",
333 serverVersion: "BIND 9.16.3",
334 socketFamily: "INET6",
335 socketProtocol: "UDP",
336 sourceAddress: "::",
337 sourcePort: 46835,
338 time: 1_593_489_007_920_014_129i64,
339 timePrecision: "ns",
340 timestamp: timestamp
341 })
342 }),
343 tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
344 }
345
346 update {
347 args: func_args![value: value!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zcmsIDhABGAEiBH8AAAEqBH8AAAEwrG44AEC+iu73BU14gfofUh1wi6gAAAEAAAAAAAAHZXhhbXBsZQNjb20AAAYAAWC+iu73BW0agDwvch1wi6gAAAEAAAAAAAAHZXhhbXBsZQNjb20AAAYAAXgB")],
348 want: Ok({
349 let timestamp = Value::Timestamp(
350 Utc.from_utc_datetime(
351 &DateTime::parse_from_rfc3339("2020-06-30T18:32:30.792494106Z")
352 .unwrap()
353 .naive_utc(),
354 ),
355 );
356 value!({
357 dataType: "Message",
358 dataTypeId: 1,
359 messageType: "UpdateResponse",
360 messageTypeId: 14,
361 requestData: {
362 fullRcode: 0,
363 header: {
364 adCount: 0,
365 id: 28811,
366 opcode: 5,
367 prCount: 0,
368 qr: 1,
369 rcode: 0,
370 upCount: 0,
371 zoCount: 1
372 },
373 zone: {
374 zClass: "IN",
375 zName: "example.com.",
376 zType: "SOA",
377 zTypeId: 6
378 },
379 rcodeName: "NoError",
380 },
381 responseAddress: "127.0.0.1",
382 responseData: {
383 fullRcode: 0,
384 header: {
385 adCount: 0,
386 id: 28811,
387 opcode: 5,
388 prCount: 0,
389 qr: 1,
390 rcode: 0,
391 upCount: 0,
392 zoCount: 1
393 },
394 zone: {
395 zClass: "IN",
396 zName: "example.com.",
397 zType: "SOA",
398 zTypeId: 6
399 },
400 rcodeName: "NoError",
401 },
402 responsePort: 0,
403 serverId: "james-Virtual-Machine",
404 serverVersion: "BIND 9.16.3",
405 socketFamily: "INET",
406 socketProtocol: "UDP",
407 sourceAddress: "127.0.0.1",
408 sourcePort: 14124,
409 time: 1_593_541_950_792_494_106i64,
410 timePrecision: "ns",
411 timestamp: timestamp
412 })
413 }),
414 tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
415 }
416
417 non_base64_value {
418 args: func_args![value: value!("non base64 string")],
419 want: Err("non base64 string is not a valid base64 encoded string"),
420 tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
421 }
422
423 invalid_dnstap_data {
424 args: func_args![value: value!("bm9uIGRuc3RhcCBkYXRh")],
425 want: Err("dnstap parsing failed for bm9uIGRuc3RhcCBkYXRh: failed to decode Protobuf message: invalid wire type value: 6"),
426 tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
427 }
428 ];
429}