]>
Commit | Line | Data |
---|---|---|
223bb49e PL |
1 | Intercepting queries with Lua |
2 | ============================= | |
3 | To get a quick start, we have supplied a sample script that showcases all functionality described below. | |
49f9972c | 4 | Please find it `here <https://github.com/PowerDNS/pdns/blob/master/pdns/recursordist/contrib/powerdns-example-script.lua>`_. |
223bb49e PL |
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 | ||
d14a6965 RG |
56 | .. function:: gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp, proxyprotocolvalues) -> int |
57 | gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp) -> int | |
ebee0255 | 58 | gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions) -> int |
223bb49e PL |
59 | |
60 | .. versionchanged:: 4.1.0 | |
61 | ||
62 | The ``tcp`` parameter was added. | |
63 | ||
f7965474 | 64 | .. versionchanged:: 4.4.0 |
d14a6965 RG |
65 | |
66 | The ``proxyprotocolvalues`` parameter was added. | |
67 | ||
223bb49e PL |
68 | The ``gettag`` function is invoked when the Recursor attempts to discover in which packetcache an answer is available. |
69 | ||
70 | This function must return an integer, which is the tag number of the packetcache. | |
71 | In addition to this integer, this function can return a table of policy tags. | |
72 | 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. | |
73 | ||
74 | .. versionadded:: 4.1.0 | |
75 | ||
adc95453 | 76 | 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. |
d14a6965 | 77 | |
cee8bfbd | 78 | .. versionadded:: 4.3.0 |
0a6a45c8 CHB |
79 | |
80 | Along the ``deviceId`` value that can be returned, it was addded a ``deviceName`` field to fill the :attr:`DNSQuestion.deviceName` field. | |
223bb49e PL |
81 | |
82 | 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`). | |
83 | This ensure that queries are answered quickly compared to setting :attr:`dq.variable <DNSQuestion.variable>` to true. | |
84 | In the latter case, repeated queries will pass through the entire Lua script. | |
85 | ||
86 | :param ComboAddress remote: The sender's IP address | |
ebee0255 PD |
87 | :param Netmask ednssubnet: The EDNS Client subnet that was extracted from the packet |
88 | :param ComboAddress localip: The IP address the query was received on | |
223bb49e PL |
89 | :param DNSName qname: The domain name the query is for |
90 | :param int qtype: The query type of the query | |
91 | :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. | |
92 | :param bool tcp: Added in 4.1.0, a boolean indicating whether the query was received over UDP (false) or TCP (true). | |
d14a6965 | 93 | :param proxyprotocolvalues: Added in 4.4.0, a table of :class:`ProxyProtocolValue` objects representing the Type-Length Values received via the Proxy Protocol, if any. |
223bb49e PL |
94 | |
95 | .. function:: prerpz(dq) | |
96 | ||
97 | 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. | |
98 | 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. | |
99 | ||
100 | As an example, to disable the "malware" policy for example.com queries: | |
101 | ||
102 | .. code-block:: Lua | |
103 | ||
104 | function prerpz(dq) | |
105 | -- disable the RPZ policy named 'malware' for example.com | |
106 | if dq.qname:equal('example.com') then | |
107 | dq:discardPolicy('malware') | |
108 | end | |
109 | return false | |
110 | end | |
111 | ||
112 | :param DNSQuestion dq: The DNS question to handle | |
113 | ||
114 | .. function:: preresolve(dq) | |
115 | ||
116 | 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. | |
117 | This is useful to combat botnets, or to disable domains unacceptable to an organization for whatever reason. | |
118 | ||
119 | :param DNSQuestion dq: The DNS question to handle | |
120 | ||
121 | .. function:: postresolve(dq) | |
122 | ||
123 | is called right before returning a response to a client (and, unless :attr:`dq.variable <DNSQuestion.variable>` is set, to the packet cache too). | |
124 | It allows inspection and modification of almost any detail in the return packet. | |
125 | ||
126 | :param DNSQuestion dq: The DNS question to handle | |
127 | ||
128 | .. function:: nxdomain(dq) | |
129 | ||
130 | is called after the DNS resolution process has run its course, but ended in an 'NXDOMAIN' situation, indicating that the domain does not exist. | |
131 | Works entirely like :func:`postresolve`, but saves a trip through Lua for answers which are not NXDOMAIN. | |
132 | ||
133 | :param DNSQuestion dq: The DNS question to handle | |
134 | ||
135 | .. function:: nodata(dq) | |
136 | ||
137 | is just like :func:`nxdomain`, except it gets called when a domain exists, but the requested type does not. | |
138 | This is where one would implement :doc:`DNS64 <../dns64>`. | |
139 | ||
140 | :param DNSQuestion dq: The DNS question to handle | |
141 | ||
142 | .. function:: preoutquery(dq) | |
143 | ||
144 | This hook is not called in response to a client packet, but fires when the Recursor wants to talk to an authoritative server. | |
145 | When this hook sets the special result code -3, the whole DNS client query causing this outquery gets dropped. | |
146 | ||
147 | However, this function can also return records like :func:`preresolve`. | |
148 | ||
149 | :param DNSQuestion dq: The DNS question to handle | |
150 | ||
151 | Semantics | |
152 | ^^^^^^^^^ | |
153 | 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. | |
154 | When a function returns ``false``, the nameserver will process the query normally until a new function is called. | |
155 | ||
156 | 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. | |
157 | An interesting rcode is NXDOMAIN (3, or ``pdns.NXDOMAIN``), which specifies the non-existence of a domain. | |
158 | ||
159 | 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. | |
160 | ||
161 | A minimal sample script: | |
162 | ||
163 | .. code-block:: Lua | |
164 | ||
165 | function nxdomain(dq) | |
166 | print("Intercepting NXDOMAIN for: ",dq.qname:toString()) | |
167 | if dq.qtype == pdns.A | |
168 | then | |
169 | dq.rcode=0 -- make it a normal answer | |
170 | dq:addAnswer(pdns.A, "192.168.1.1") | |
171 | return true | |
172 | end | |
173 | return false | |
174 | end | |
175 | ||
176 | **Warning**: Please do NOT use the above sample script in production! | |
177 | Responsible NXDomain redirection requires more attention to detail. | |
178 | ||
179 | Useful 'rcodes' include 0 for "no error", ``pdns.NXDOMAIN`` for "NXDOMAIN", ``pdns.DROP`` to drop the question from further processing. | |
180 | Such a drop is accounted in the 'policy-drops' metric. | |
181 | ||
182 | DNS64 | |
183 | ----- | |
184 | ||
185 | The ``getFakeAAAARecords`` and ``getFakePTRRecords`` followupFunctions | |
cf3e4fab | 186 | can be used to implement DNS64. See :doc:`../dns64` for more information. |
223bb49e PL |
187 | |
188 | To get fake AAAA records for DNS64 usage, set dq.followupFunction to | |
189 | ``getFakeAAAARecords``, dq.followupPrefix to e.g. "64:ff9b::" and | |
190 | dq.followupName to the name you want to synthesize an IPv6 address for. | |
191 | ||
192 | For fake reverse (PTR) records, set dq.followupFunction to | |
193 | ``getFakePTRRecords`` and set dq.followupName to the name to look up and | |
194 | dq.followupPrefix to the same prefix as used with | |
195 | ``getFakeAAAARecords``. | |
196 | ||
197 | Follow up actions | |
198 | ----------------- | |
199 | When modifying queries, it might be needed that the Recursor does some extra work after the function returns. | |
200 | The :attr:`dq.followupFunction <DNSQuestion.followupFunction>` can be set in this case. | |
201 | ||
202 | .. _cnamechainresolution: | |
203 | ||
204 | CNAME chain resolution | |
205 | ^^^^^^^^^^^^^^^^^^^^^^ | |
206 | It may be useful to return a CNAME record for Lua, and then have the PowerDNS Recursor continue resolving that CNAME. | |
207 | This can be achieved by setting dq.followupFunction to ``followCNAMERecords`` and dq.followupDomain to "www.powerdns.com". | |
208 | PowerDNS will do the rest. | |
209 | ||
210 | .. _udpqueryresponse: | |
211 | ||
212 | UDP Query Response | |
213 | ^^^^^^^^^^^^^^^^^^ | |
214 | The ``udpQueryResponse`` :attr:`dq.followupFunction <DNSQuestion.followupFunction>` allows you to query a simple key-value store over UDP asynchronously. | |
215 | ||
216 | Several dq variables can be set: | |
217 | ||
218 | - :attr:`dq.udpQueryDest <DNSQuestion.udpQueryDest>`: destination IP address to send the UDP packet to | |
219 | - :attr:`dq.udpQuery <DNSQuestion.udpQuery>`: The content of the UDP payload | |
220 | - :attr:`dq.udpCallback <DNSQuestion.udpCallback>`: The name of the callback function that is called when an answer is received | |
221 | ||
222 | The callback function must accept the ``dq`` object and can find the response to the UDP query in :attr:`dq.udpAnswer <DNSQuestion.udpAnswer>`. | |
223 | ||
224 | In this callback function, :attr:`dq.followupFunction <DNSQuestion.followupFunction>` can be set again to any of the available functions for further processing. | |
225 | ||
226 | This example script queries a simple key/value store over UDP to decide on whether or not to filter a query: | |
227 | ||
228 | .. literalinclude:: ../../contrib/kv-example-script.lua | |
229 | :language: Lua | |
230 | ||
231 | Example Script | |
232 | -------------- | |
233 | ||
234 | .. literalinclude:: ../../contrib/powerdns-example-script.lua | |
235 | :language: Lua | |
236 | ||
237 | Dropping all traffic from botnet-infected users | |
238 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | |
239 | Frequently, DoS attacks are performed where specific IP addresses are attacked, often by queries coming in from open resolvers. | |
240 | 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. | |
241 | ||
242 | 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. | |
243 | ||
244 | This specific script is, as of January 2015, useful to prevent traffic to ezdns.it related traffic from creating CPU load. | |
245 | This script requires PowerDNS Recursor 4.x or later. | |
246 | ||
247 | .. code-block:: Lua | |
248 | ||
249 | lethalgroup=newNMG() | |
250 | lethalgroup:addMask("192.121.121.0/24") -- touch these nameservers and you die | |
251 | ||
252 | function preoutquery(dq) | |
253 | print("pdns wants to ask "..dq.remoteaddr:toString().." about "..dq.qname:toString().." "..dq.qtype.." on behalf of requestor "..dq.localaddr:toString()) | |
254 | if(lethalgroup:match(dq.remoteaddr)) | |
255 | then | |
256 | print("We matched the group "..lethalgroup:tostring().."!", "killing query dead & adding requestor "..dq.localaddr:toString().." to block list") | |
257 | dq.rcode = -3 -- "kill" | |
258 | return true | |
259 | end | |
260 | return false | |
261 | end | |
262 | ||
263 | .. _modifyingpolicydecisions: | |
264 | ||
265 | Modifying Policy Decisions | |
266 | -------------------------- | |
267 | The PowerDNS Recursor has a :doc:`policy engine based on Response Policy Zones (RPZ) <../lua-config/rpz>`. | |
268 | Starting with version 4.0.1 of the recursor, it is possible to alter this decision inside the Lua hooks. | |
269 | ||
270 | 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. | |
271 | The result of the policy decision is checked after :func:`preresolve` and :func:`postresolve`. | |
272 | ||
273 | 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`. | |
274 | ||
275 | Example script | |
276 | ^^^^^^^^^^^^^^ | |
277 | ||
278 | .. code-block:: Lua | |
279 | ||
280 | -- Dont ever block my own domain and IPs | |
281 | myDomain = newDN("example.com") | |
282 | ||
283 | myNetblock = newNMG() | |
e94b05b0 | 284 | myNetblock:addMasks({"192.0.2.0/24"}) |
223bb49e PL |
285 | |
286 | function preresolve(dq) | |
287 | if dq.qname:isPartOf(myDomain) and dq.appliedPolicy.policyKind ~= pdns.policykinds.NoAction then | |
288 | pdnslog("Not blocking our own domain!") | |
289 | dq.appliedPolicy.policyKind = pdns.policykinds.NoAction | |
290 | end | |
e94b05b0 | 291 | return false |
223bb49e PL |
292 | end |
293 | ||
294 | function postresolve(dq) | |
295 | if dq.appliedPolicy.policyKind ~= pdns.policykinds.NoAction then | |
296 | local records = dq:getRecords() | |
297 | for k,v in pairs(records) do | |
298 | if v.type == pdns.A then | |
299 | local blockedIP = newCA(v:getContent()) | |
300 | if myNetblock:match(blockedIP) then | |
301 | pdnslog("Not blocking our IP space") | |
302 | dq.appliedPolicy.policyKind = pdns.policykinds.NoAction | |
303 | end | |
304 | end | |
305 | end | |
306 | end | |
e94b05b0 | 307 | return false |
223bb49e PL |
308 | end |
309 | ||
4368d62f PL |
310 | .. _snmp: |
311 | ||
223bb49e PL |
312 | SNMP Traps |
313 | ---------- | |
314 | ||
315 | PowerDNS Recursor, when compiled with SNMP support, has the ability to | |
316 | act as a SNMP agent to provide SNMP statistics and to be able to send | |
317 | traps from Lua. | |
318 | ||
319 | For example, to send a custom SNMP trap containing the qname from the | |
320 | ``preresolve`` hook: | |
321 | ||
322 | .. code-block:: Lua | |
323 | ||
324 | function preresolve(dq) | |
325 | sendCustomSNMPTrap('Trap from preresolve, qname is '..dq.qname:toString()) | |
326 | return false | |
327 | end | |
4368d62f | 328 | |
a2f87dd1 CHB |
329 | .. _hooks-maintenance-callback: |
330 | ||
331 | Maintenance callback | |
332 | -------------------- | |
2a9a7388 PD |
333 | Starting with version 4.2.0 of the recursor, it is possible to define a `maintenance()` callback function that will be called periodically. |
334 | This function expects no argument and doesn't return any value. | |
a2f87dd1 CHB |
335 | |
336 | .. code-block:: Lua | |
337 | ||
338 | function maintenance() | |
339 | -- This would be called every second | |
340 | -- Perform here your maintenance | |
341 | end | |
342 | ||
343 | The interval can be configured through the :ref:`setting-maintenance-interval` setting. |