]>
gitweb.michael.orlitzky.com - djbdns-logparse.git/blob - djbdns/dnscache.py
3dd6d5f4ac6282bd6aff3aa6610d4a84ef797715
2 Functions and data specific to dnscache logs.
4 # Don't clobber the global compile() with a named import.
7 from typing
import Optional
8 from djbdns
.common
import QUERY_TYPE_NAME
, TIMESTAMP_PAT
, convert_ip
10 # The regex to match dnscache log lines.
11 DNSCACHE_LOG_RE
= re
.compile(fr
'({TIMESTAMP_PAT}) (\w+)(.*)')
14 def decode_client(words
: list, i
: int):
16 Helper function to decode the client field in a dnscache log
19 There are two possible formats for the client field,
21 1. clientip:clientport, used by tcpopen/tcpclose entries,
22 2. clientip:clientport:id, used by "query" entries.
24 We convert each part from hex to decimal, and in the second
25 format, separate the packet id from the client information.
31 The ``words`` list (a list of fields) from
32 :func:`handle_dnscache_log`.
35 The index of the client field within ``words``
40 Nothing; the ``i``th entry in the ``words`` list is modified
46 >>> words = ["foo", "bar", "7f000001:9253", "quux"]
47 >>> decode_client(words, 2)
49 ['foo', 'bar', '127.0.0.1:37459', 'quux']
51 >>> words = ["foo", "7f000001:a3db:4fb9", "bar", "quux"]
52 >>> decode_client(words, 1)
54 ['foo', '127.0.0.1:41947 (id 20409)', 'bar', 'quux']
57 chunks
= words
[i
].split(":")
59 ip
= convert_ip(chunks
[0])
60 port
= int(chunks
[1], 16)
61 words
[i
] = f
"{ip}:{port}"
64 # For a "query" entry's clientip:clientport:id field.
65 packet_id
= int(chunks
[2], 16)
66 words
[i
] += f
" (id {packet_id})"
68 def decode_ip(words
: list, i
: int):
70 Helper function to decode the ip field in a dnscache log
73 A single "serverip" field is present in the lame, nodata,
74 nxdomain, and rr entry types. We convert it from hex to decimal.
80 The ``words`` list (a list of fields) from
81 :func:`handle_dnscache_log`.
84 The index of the ip field within ``words``
89 Nothing; the ``i``th entry in the ``words`` list is modified
95 >>> words = ["foo", "bar", "7f000001", "quux"]
96 >>> decode_ip(words, 2)
98 ['foo', 'bar', '127.0.0.1', 'quux']
100 >>> words = ["foo", "00000000000000000000ffff7f000001", "bar", "quux"]
101 >>> decode_ip(words, 1)
103 ['foo', '0000:0000:0000:0000:0000:ffff:7f00:0001', 'bar', 'quux']
105 words
[i
] = convert_ip(words
[i
])
107 def decode_ttl(words
: list, i
: int):
109 Helper function to decode the ttl field in a dnscache log
112 A single "ttl" field is present in the nodata, nxdomain, and
113 rr entry types. We prefix it with "TTL=" so that its meaning
114 is clear in the human-readable logs.
120 The ``words`` list (a list of fields) from
121 :func:`handle_dnscache_log`.
124 The index of the ttl field within ``words``
129 Nothing; the ``i``th entry in the ``words`` list is modified
135 >>> words = ["c0a80101", "20865", "1", "www.example.com.", "5db8d822"]
136 >>> decode_ttl(words, 1)
138 ['c0a80101', 'TTL=20865', '1', 'www.example.com.', '5db8d822']
141 words
[i
] = f
"TTL={words[i]}"
143 def decode_serial(words
: list, i
: int):
145 Helper function to decode the serial field in a dnscache log
148 A single "serial" field is present in the drop and query entry
149 types. It's already in decimal; we simply prefix it with a hash.
155 The ``words`` list (a list of fields) from
156 :func:`handle_dnscache_log`.
159 The index of the serial field within ``words``
164 Nothing; the ``i``th entry in the ``words`` list is modified
170 >>> words = ["1", "7f000001:a3db:4fb9", "1", "www.example.com."]
171 >>> decode_serial(words, 0)
173 ['#1', '7f000001:a3db:4fb9', '1', 'www.example.com.']
176 words
[i
] = f
"#{words[i]}"
178 def decode_type(words
: list, i
: int):
180 Helper function to decode the type field in a dnscache log
183 A single "type" field is present in cached, nodata, query, rr, and
184 tx entries. Unlike with tinydns entries, dnscache logs have
185 this field already in decimal, so we just look up the
186 corresponding name in the query type map.
192 A list with the "type" string at index ``i``
195 The index of the type field within ``words``
200 Nothing; the ``i``th entry in the ``words`` list is modified
206 >>> words = ["2", "7f000001:b848:0f0b", "16", "example.com."]
207 >>> decode_type(words, 2)
209 ['2', '7f000001:b848:0f0b', 'txt', 'example.com.']
213 words
[i
] = QUERY_TYPE_NAME
.get(int(qt
), qt
)
215 def handle_dnscache_log(line
: str) -> Optional
[str]:
217 Handle a single log line if it matches the ``DNSCACHE_LOG_RE`` regex.
223 The log line that might match ``DNSCACHE_LOG_RE``.
228 Either the human-readable string if the log line was handled (that
229 is, if it was really a dnscache log line), or ``None`` if it was
235 >>> line = "2022-09-15 18:37:33.863805500 query 1 7f000001:a3db:4fb9 1 www.example.com."
236 >>> handle_dnscache_log(line)
237 '2022-09-15 18:37:33.863805500 query #1 127.0.0.1:41947 (id 20409) a www.example.com.'
239 >>> line = "2022-09-15 18:37:33.863874500 tx 0 1 www.example.com. . c0a80101"
240 >>> handle_dnscache_log(line)
241 '2022-09-15 18:37:33.863874500 tx g=0 a www.example.com. . 192.168.1.1'
243 >>> line = "2022-09-15 18:37:33.878529500 rr c0a80101 20865 1 www.example.com. 5db8d822"
244 >>> handle_dnscache_log(line)
245 '2022-09-15 18:37:33.878529500 rr 192.168.1.1 TTL=20865 a www.example.com. 93.184.216.34'
247 >>> line = "2022-09-15 18:37:33.878532500 stats 1 43 1 0"
248 >>> handle_dnscache_log(line)
249 '2022-09-15 18:37:33.878532500 stats count=1 motion=43 udp-active=1 tcp-active=0'
251 >>> line = "2022-09-15 18:37:33.878602500 sent 1 49"
252 >>> handle_dnscache_log(line)
253 '2022-09-15 18:37:33.878602500 sent #1 49'
255 >>> line = "this line is nonsense"
256 >>> handle_dnscache_log(line)
259 match
= DNSCACHE_LOG_RE
.match(line
)
263 (timestamp
, event
, data
) = match
.groups()
266 if event
== "cached":
267 if words
[0] not in ("cname", "ns", "nxdomain"):
268 decode_type(words
, 0)
270 elif event
== "drop":
271 decode_serial(words
, 0)
273 elif event
== "lame":
276 elif event
== "nodata":
279 decode_type(words
, 2)
281 elif event
== "nxdomain":
285 elif event
== "query":
286 decode_serial(words
, 0)
287 decode_client(words
, 1)
288 decode_type(words
, 2)
293 if words
[2] not in ("cname", "mx", "ns", "ptr", "soa"):
294 decode_type(words
, 2)
297 # Decode the response to an 'A' query
299 if words
[2] == "txt":
300 # Decode the TXT record's data from hex to ASCII.
302 if response
.endswith("..."):
304 response
= response
[0:-3]
307 length
= int(response
[0:2], 16)
309 for i
in range(1, len(response
)//2):
310 chars
.append(chr(int(response
[2*i
: (2*i
)+2], 16)))
312 words
[4] = f
"{length}:\"{txt}{ellipsis}\""
314 elif event
== "sent":
315 decode_serial(words
, 0)
317 elif event
== "stats":
318 words
[0] = f
"count={words[0]}"
319 words
[1] = f
"motion={words[1]}"
320 words
[2] = f
"udp-active={words[2]}"
321 words
[3] = f
"tcp-active={words[3]}"
324 words
[0] = f
"g={words[0]}"
325 decode_type(words
, 1)
327 # words[3] = control (domain for which these servers are believed
328 # to be authoritative)
329 for i
in range(4, len(words
)):
332 elif event
in ("tcpopen", "tcpclose"):
333 decode_client(words
, 0)
335 # Reconstitute "data" (i.e. everything after the timestamp and the
336 # event) from "words", which was originally obtained by splitting
338 data
= " ".join(words
)
339 return f
"{timestamp} {event} {data}"