]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/recursordist/docs/lua-scripting/hooks.rst
79ec8b0e5592ad57e5aab3e46026ef077432ae10
[thirdparty/pdns.git] / pdns / recursordist / docs / lua-scripting / hooks.rst
1 Intercepting queries with Lua
2 =============================
3 To get a quick start, we have supplied a sample script that showcases all functionality described below.
4 Please find it `here <https://github.com/PowerDNS/pdns/blob/master/pdns/recursordist/contrib/powerdns-example-script.lua>`_.
5
6 Queries can be intercepted in many places:
7
8 - before any packet parsing begins (:func:`ipfilter`)
9 - before any filtering policy have been applied (:func:`prerpz`)
10 - before the resolving logic starts to work (:func:`preresolve`)
11 - after the resolving process failed to find a correct answer for a domain (:func:`nodata`, :func:`nxdomain`)
12 - after the whole process is done and an answer is ready for the client (:func:`postresolve`)
13 - before an outgoing query is made to an authoritative server (:func:`preoutquery`)
14
15 Writing Lua PowerDNS Recursor scripts
16 -------------------------------------
17 Addresses and DNS Names are not passed as strings but as native objects.
18 This allows for easy checking against `Netmasks <scripting-netmasks>`_ and `domain sets <scripting-dnsname>`_.
19 It also means that to print such names, the ``:toString`` method must be used (or even ``:toStringWithPort`` for addresses).
20
21 Once a script is loaded, PowerDNS looks for several `functions <scripting-hooks>`_ in the loaded script.
22 All of these functions are optional.
23
24 If a function returns true, it will indicate that it handled a query.
25 If it returns false, the Recursor will continue processing unchanged (with one minor exception).
26
27 Interception Functions
28 ----------------------
29
30 .. function:: ipfilter(remoteip, localip, dh) -> bool
31
32 This hook gets queried immediately after consulting the packet cache, but before parsing the DNS packet.
33 If this hook returns something else than false, the packet is dropped.
34 However, because this check is after the packet cache, the IP address might still receive answers that require no packet parsing.
35
36 With this hook, undesired traffic can be dropped rapidly before using precious CPU cycles for parsing.
37 As an example, to filter all queries coming from 1.2.3.0/24, or with the
38 AD bit set:
39
40 .. code-block:: Lua
41
42 badips = newNMG()
43 badips:addMask("1.2.3.0/24")
44
45 function ipfilter(rem, loc, dh)
46 return badips:match(rem) or dh:getAD()
47 end
48
49 This hook does not get the full :class:`DNSQuestion` object, since filling out the fields would require packet parsing, which is what we are trying to prevent with this function.
50
51 :param ComboAddress remoteip: The IP(v6) address of the requestor
52 :param ComboAddress localip: The address on which the query arrived.
53 :param DNSHeader dh: The DNS Header of the query.
54
55
56 .. function:: gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp) -> int
57 gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions) -> int
58
59 .. versionchanged:: 4.1.0
60
61 The ``tcp`` parameter was added.
62
63 The ``gettag`` function is invoked when the Recursor attempts to discover in which packetcache an answer is available.
64
65 This function must return an integer, which is the tag number of the packetcache.
66 In addition to this integer, this function can return a table of policy tags.
67 The resulting tag number can be accessed via :attr:`dq.tag <DNSQuestion.tag>` in the :func:`preresolve` hook, and the policy tags via :meth:`dq:getPolicyTags() <DNSQuestion:getPolicyTags>` in every hook.
68
69 .. versionadded:: 4.1.0
70
71 It can also return a table whose keys and values are strings to fill the :attr:`DNSQuestion.data` table, as well as a ``requestorId`` value to fill the :attr:`DNSQuestion.requestorId` field and a ``deviceId`` value to fill the :attr:`DNSQuestion.deviceId` field.
72 .. versionadded:: 4.3.0
73
74 Along the ``deviceId`` value that can be returned, it was addded a ``deviceName`` field to fill the :attr:`DNSQuestion.deviceName` field.
75
76 The tagged packetcache can e.g. be used to answer queries from cache that have e.g. been filtered for certain IPs (this logic should be implemented in :func:`gettag`).
77 This ensure that queries are answered quickly compared to setting :attr:`dq.variable <DNSQuestion.variable>` to true.
78 In the latter case, repeated queries will pass through the entire Lua script.
79
80 :param ComboAddress remote: The sender's IP address
81 :param Netmask ednssubnet: The EDNS Client subnet that was extracted from the packet
82 :param ComboAddress localip: The IP address the query was received on
83 :param DNSName qname: The domain name the query is for
84 :param int qtype: The query type of the query
85 :param ednsoptions: A table whose keys are EDNS option codes and values are :class:`EDNSOptionView` objects. This table is empty unless the :ref:`setting-gettag-needs-edns-options` option is set.
86 :param bool tcp: Added in 4.1.0, a boolean indicating whether the query was received over UDP (false) or TCP (true).
87
88 .. function:: prerpz(dq)
89
90 This hook is called before any filtering policy have been applied, making it possible to completely disable filtering by setting :attr:`dq.wantsRPZ <DNSQuestion.wantsRPZ>` to false.
91 Using the :meth:`dq:discardPolicy() <DNSQuestion:discardPolicy>` function, it is also possible to selectively disable one or more filtering policy, for example RPZ zones, based on the content of the ``dq`` object.
92
93 As an example, to disable the "malware" policy for example.com queries:
94
95 .. code-block:: Lua
96
97 function prerpz(dq)
98 -- disable the RPZ policy named 'malware' for example.com
99 if dq.qname:equal('example.com') then
100 dq:discardPolicy('malware')
101 end
102 return false
103 end
104
105 :param DNSQuestion dq: The DNS question to handle
106
107 .. function:: preresolve(dq)
108
109 This function is called before any DNS resolution is attempted, and if this function indicates it, it can supply a direct answer to the DNS query, overriding the internet.
110 This is useful to combat botnets, or to disable domains unacceptable to an organization for whatever reason.
111
112 :param DNSQuestion dq: The DNS question to handle
113
114 .. function:: postresolve(dq)
115
116 is called right before returning a response to a client (and, unless :attr:`dq.variable <DNSQuestion.variable>` is set, to the packet cache too).
117 It allows inspection and modification of almost any detail in the return packet.
118
119 :param DNSQuestion dq: The DNS question to handle
120
121 .. function:: nxdomain(dq)
122
123 is called after the DNS resolution process has run its course, but ended in an 'NXDOMAIN' situation, indicating that the domain does not exist.
124 Works entirely like :func:`postresolve`, but saves a trip through Lua for answers which are not NXDOMAIN.
125
126 :param DNSQuestion dq: The DNS question to handle
127
128 .. function:: nodata(dq)
129
130 is just like :func:`nxdomain`, except it gets called when a domain exists, but the requested type does not.
131 This is where one would implement :doc:`DNS64 <../dns64>`.
132
133 :param DNSQuestion dq: The DNS question to handle
134
135 .. function:: preoutquery(dq)
136
137 This hook is not called in response to a client packet, but fires when the Recursor wants to talk to an authoritative server.
138 When this hook sets the special result code -3, the whole DNS client query causing this outquery gets dropped.
139
140 However, this function can also return records like :func:`preresolve`.
141
142 :param DNSQuestion dq: The DNS question to handle
143
144 Semantics
145 ^^^^^^^^^
146 The functions must return ``true`` if they have taken over the query and wish that the nameserver should not proceed with its regular query-processing.
147 When a function returns ``false``, the nameserver will process the query normally until a new function is called.
148
149 If a function has taken over a request, it should set an rcode (usually 0), and specify a table with records to be put in the answer section of a packet.
150 An interesting rcode is NXDOMAIN (3, or ``pdns.NXDOMAIN``), which specifies the non-existence of a domain.
151
152 The :func:`ipfilter` and :func:`preoutquery` hooks are different, in that :func:`ipfilter` can only return a true of false value, and that :func:`preoutquery` can also set rcode -3 to signify that the whole query should be terminated.
153
154 A minimal sample script:
155
156 .. code-block:: Lua
157
158 function nxdomain(dq)
159 print("Intercepting NXDOMAIN for: ",dq.qname:toString())
160 if dq.qtype == pdns.A
161 then
162 dq.rcode=0 -- make it a normal answer
163 dq:addAnswer(pdns.A, "192.168.1.1")
164 return true
165 end
166 return false
167 end
168
169 **Warning**: Please do NOT use the above sample script in production!
170 Responsible NXDomain redirection requires more attention to detail.
171
172 Useful 'rcodes' include 0 for "no error", ``pdns.NXDOMAIN`` for "NXDOMAIN", ``pdns.DROP`` to drop the question from further processing.
173 Such a drop is accounted in the 'policy-drops' metric.
174
175 DNS64
176 -----
177
178 The ``getFakeAAAARecords`` and ``getFakePTRRecords`` followupFunctions
179 can be used to implement DNS64. See :doc:`../dns64` for more information.
180
181 To get fake AAAA records for DNS64 usage, set dq.followupFunction to
182 ``getFakeAAAARecords``, dq.followupPrefix to e.g. "64:ff9b::" and
183 dq.followupName to the name you want to synthesize an IPv6 address for.
184
185 For fake reverse (PTR) records, set dq.followupFunction to
186 ``getFakePTRRecords`` and set dq.followupName to the name to look up and
187 dq.followupPrefix to the same prefix as used with
188 ``getFakeAAAARecords``.
189
190 Follow up actions
191 -----------------
192 When modifying queries, it might be needed that the Recursor does some extra work after the function returns.
193 The :attr:`dq.followupFunction <DNSQuestion.followupFunction>` can be set in this case.
194
195 .. _cnamechainresolution:
196
197 CNAME chain resolution
198 ^^^^^^^^^^^^^^^^^^^^^^
199 It may be useful to return a CNAME record for Lua, and then have the PowerDNS Recursor continue resolving that CNAME.
200 This can be achieved by setting dq.followupFunction to ``followCNAMERecords`` and dq.followupDomain to "www.powerdns.com".
201 PowerDNS will do the rest.
202
203 .. _udpqueryresponse:
204
205 UDP Query Response
206 ^^^^^^^^^^^^^^^^^^
207 The ``udpQueryResponse`` :attr:`dq.followupFunction <DNSQuestion.followupFunction>` allows you to query a simple key-value store over UDP asynchronously.
208
209 Several dq variables can be set:
210
211 - :attr:`dq.udpQueryDest <DNSQuestion.udpQueryDest>`: destination IP address to send the UDP packet to
212 - :attr:`dq.udpQuery <DNSQuestion.udpQuery>`: The content of the UDP payload
213 - :attr:`dq.udpCallback <DNSQuestion.udpCallback>`: The name of the callback function that is called when an answer is received
214
215 The callback function must accept the ``dq`` object and can find the response to the UDP query in :attr:`dq.udpAnswer <DNSQuestion.udpAnswer>`.
216
217 In this callback function, :attr:`dq.followupFunction <DNSQuestion.followupFunction>` can be set again to any of the available functions for further processing.
218
219 This example script queries a simple key/value store over UDP to decide on whether or not to filter a query:
220
221 .. literalinclude:: ../../contrib/kv-example-script.lua
222 :language: Lua
223
224 Example Script
225 --------------
226
227 .. literalinclude:: ../../contrib/powerdns-example-script.lua
228 :language: Lua
229
230 Dropping all traffic from botnet-infected users
231 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
232 Frequently, DoS attacks are performed where specific IP addresses are attacked, often by queries coming in from open resolvers.
233 These queries then lead to a lot of queries to 'authoritative servers' which actually often aren't nameservers at all, but just targets of attack.
234
235 The following script will add a requestor's IP address to a blocking set if they've sent a query that caused PowerDNS to attempt to talk to a certain subnet.
236
237 This specific script is, as of January 2015, useful to prevent traffic to ezdns.it related traffic from creating CPU load.
238 This script requires PowerDNS Recursor 4.x or later.
239
240 .. code-block:: Lua
241
242 lethalgroup=newNMG()
243 lethalgroup:addMask("192.121.121.0/24") -- touch these nameservers and you die
244
245 function preoutquery(dq)
246 print("pdns wants to ask "..dq.remoteaddr:toString().." about "..dq.qname:toString().." "..dq.qtype.." on behalf of requestor "..dq.localaddr:toString())
247 if(lethalgroup:match(dq.remoteaddr))
248 then
249 print("We matched the group "..lethalgroup:tostring().."!", "killing query dead & adding requestor "..dq.localaddr:toString().." to block list")
250 dq.rcode = -3 -- "kill"
251 return true
252 end
253 return false
254 end
255
256 .. _modifyingpolicydecisions:
257
258 Modifying Policy Decisions
259 --------------------------
260 The PowerDNS Recursor has a :doc:`policy engine based on Response Policy Zones (RPZ) <../lua-config/rpz>`.
261 Starting with version 4.0.1 of the recursor, it is possible to alter this decision inside the Lua hooks.
262
263 If the decision is modified in a Lua hook, ``false`` should be returned, as the query is not actually handled by Lua so the decision is picked up by the Recursor.
264 The result of the policy decision is checked after :func:`preresolve` and :func:`postresolve`.
265
266 For example, if a decision is set to ``pdns.policykinds.NODATA`` by the policy engine and is unchanged in :func:`preresolve`, the query is replied to with a NODATA response immediately after :func:`preresolve`.
267
268 Example script
269 ^^^^^^^^^^^^^^
270
271 .. code-block:: Lua
272
273 -- Dont ever block my own domain and IPs
274 myDomain = newDN("example.com")
275
276 myNetblock = newNMG()
277 myNetblock:addMasks({"192.0.2.0/24"})
278
279 function preresolve(dq)
280 if dq.qname:isPartOf(myDomain) and dq.appliedPolicy.policyKind ~= pdns.policykinds.NoAction then
281 pdnslog("Not blocking our own domain!")
282 dq.appliedPolicy.policyKind = pdns.policykinds.NoAction
283 end
284 return false
285 end
286
287 function postresolve(dq)
288 if dq.appliedPolicy.policyKind ~= pdns.policykinds.NoAction then
289 local records = dq:getRecords()
290 for k,v in pairs(records) do
291 if v.type == pdns.A then
292 local blockedIP = newCA(v:getContent())
293 if myNetblock:match(blockedIP) then
294 pdnslog("Not blocking our IP space")
295 dq.appliedPolicy.policyKind = pdns.policykinds.NoAction
296 end
297 end
298 end
299 end
300 return false
301 end
302
303 .. _snmp:
304
305 SNMP Traps
306 ----------
307
308 PowerDNS Recursor, when compiled with SNMP support, has the ability to
309 act as a SNMP agent to provide SNMP statistics and to be able to send
310 traps from Lua.
311
312 For example, to send a custom SNMP trap containing the qname from the
313 ``preresolve`` hook:
314
315 .. code-block:: Lua
316
317 function preresolve(dq)
318 sendCustomSNMPTrap('Trap from preresolve, qname is '..dq.qname:toString())
319 return false
320 end
321
322 .. _hooks-maintenance-callback:
323
324 Maintenance callback
325 --------------------
326 Starting with version 4.1.4 of the recursor, it is possible to define a `maintenance()` callback function that will be called periodically.
327 This function expects no argument and doesn't return any value
328
329 .. code-block:: Lua
330
331 function maintenance()
332 -- This would be called every second
333 -- Perform here your maintenance
334 end
335
336 The interval can be configured through the :ref:`setting-maintenance-interval` setting.