1# -*- coding: utf-8 -*- 2''' 3 inplace_callbacks.py: python module showcasing inplace callback function 4 registration and functionality. 5 6 Copyright (c) 2016, NLnet Labs. 7 8 This software is open source. 9 10 Redistribution and use in source and binary forms, with or without 11 modification, are permitted provided that the following conditions 12 are met: 13 14 * Redistributions of source code must retain the above copyright notice, 15 this list of conditions and the following disclaimer. 16 17 * Redistributions in binary form must reproduce the above copyright notice, 18 this list of conditions and the following disclaimer in the documentation 19 and/or other materials provided with the distribution. 20 21 * Neither the name of the organization nor the names of its 22 contributors may be used to endorse or promote products derived from this 23 software without specific prior written permission. 24 25 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 27 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 28 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE 29 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 POSSIBILITY OF SUCH DAMAGE. 36''' 37 38import os 39 40#Try: 41# - dig @localhost nlnetlabs.nl +ednsopt=65002: 42# This query *could* be answered from cache. If so, unbound will reply 43# with the same EDNS option 65002, but with hexdata 'deadbeef' as data. 44# 45# - dig @localhost bogus.nlnetlabs.nl txt: 46# This query returns SERVFAIL as the txt record of bogus.nlnetlabs.nl is 47# intentionally bogus. The reply will contain an empty EDNS option 48# with option code 65003. 49# Unbound will also log the source address of the client that made 50# the request. 51# (unbound needs to be validating for this example to work) 52 53# Useful functions: 54# register_inplace_cb_reply(inplace_reply_callback, env, id): 55# Register the reply_callback function as an inplace callback function 56# when answering with a resolved query. 57# Return True on success, False on failure. 58# 59# register_inplace_cb_reply_cache(inplace_reply_cache_callback, env, id): 60# Register the reply_cache_callback function as an inplace callback 61# function when answering from cache. 62# Return True on success, False on failure. 63# 64# register_inplace_cb_reply_local(inplace_reply_local_callback, env, id): 65# Register the reply_local_callback function as an inplace callback 66# function when answering from local data or chaos reply. 67# Return True on success, False on failure. 68# 69# register_inplace_cb_reply_servfail(inplace_reply_servfail_callback, env, id): 70# Register the reply_servfail_callback function as an inplace callback 71# function when answering with servfail. 72# Return True on success, False on failure. 73# 74# Examples on how to use the functions are given in this file. 75 76 77def inplace_reply_callback(qinfo, qstate, rep, rcode, edns, opt_list_out, 78 region, **kwargs): 79 """ 80 Function that will be registered as an inplace callback function. 81 It will be called when answering with a resolved query. 82 83 :param qinfo: query_info struct; 84 :param qstate: module qstate. It contains the available opt_lists; It 85 SHOULD NOT be altered; 86 :param rep: reply_info struct; 87 :param rcode: return code for the query; 88 :param edns: edns_data to be sent to the client side. It SHOULD NOT be 89 altered; 90 :param opt_list_out: the list with the EDNS options that will be sent as a 91 reply. It can be populated with EDNS options; 92 :param region: region to allocate temporary data. Needs to be used when we 93 want to append a new option to opt_list_out. 94 :param **kwargs: Dictionary that may contain parameters added in a future 95 release. Current parameters: 96 ``repinfo``: Reply information for a communication point (comm_reply). 97 98 :return: True on success, False on failure. 99 100 """ 101 log_info("python: called back while replying.") 102 return True 103 104 105def inplace_cache_callback(qinfo, qstate, rep, rcode, edns, opt_list_out, 106 region, **kwargs): 107 """ 108 Function that will be registered as an inplace callback function. 109 It will be called when answering from the cache. 110 111 :param qinfo: query_info struct; 112 :param qstate: module qstate. None; 113 :param rep: reply_info struct; 114 :param rcode: return code for the query; 115 :param edns: edns_data sent from the client side. The list with the EDNS 116 options is accessible through edns.opt_list. It SHOULD NOT be 117 altered; 118 :param opt_list_out: the list with the EDNS options that will be sent as a 119 reply. It can be populated with EDNS options; 120 :param region: region to allocate temporary data. Needs to be used when we 121 want to append a new option to opt_list_out. 122 :param **kwargs: Dictionary that may contain parameters added in a future 123 release. Current parameters: 124 ``repinfo``: Reply information for a communication point (comm_reply). 125 126 :return: True on success, False on failure. 127 128 For demonstration purposes we want to see if EDNS option 65002 is present 129 and reply with a new value. 130 131 """ 132 log_info("python: called back while answering from cache.") 133 # Inspect the incoming EDNS options. 134 if not edns_opt_list_is_empty(edns.opt_list): 135 log_info("python: available EDNS options:") 136 for o in edns.opt_list_iter: 137 log_info("python: Code: {}, Data: '{}'".format(o.code, 138 "".join('{:02x}'.format(x) for x in o.data))) 139 if o.code == 65002: 140 log_info("python: *found option code 65002*") 141 142 # add to opt_list 143 # Data MUST be represented in a bytearray. 144 b = bytearray.fromhex("deadbeef") 145 if edns_opt_list_append(opt_list_out, o.code, b, region): 146 log_info("python: *added new option code 65002*") 147 else: 148 log_info("python: *failed to add new option code 65002*") 149 return False 150 break 151 152 return True 153 154 155def inplace_local_callback(qinfo, qstate, rep, rcode, edns, opt_list_out, 156 region, **kwargs): 157 """ 158 Function that will be registered as an inplace callback function. 159 It will be called when answering from local data. 160 161 :param qinfo: query_info struct; 162 :param qstate: module qstate. None; 163 :param rep: reply_info struct; 164 :param rcode: return code for the query; 165 :param edns: edns_data sent from the client side. The list with the 166 EDNS options is accessible through edns.opt_list. It 167 SHOULD NOT be altered; 168 :param opt_list_out: the list with the EDNS options that will be sent as a 169 reply. It can be populated with EDNS options; 170 :param region: region to allocate temporary data. Needs to be used when we 171 want to append a new option to opt_list_out. 172 :param **kwargs: Dictionary that may contain parameters added in a future 173 release. Current parameters: 174 ``repinfo``: Reply information for a communication point (comm_reply). 175 176 :return: True on success, False on failure. 177 178 """ 179 log_info("python: called back while replying with local data or chaos" 180 " reply.") 181 return True 182 183 184def inplace_servfail_callback(qinfo, qstate, rep, rcode, edns, opt_list_out, 185 region, **kwargs): 186 """ 187 Function that will be registered as an inplace callback function. 188 It will be called when answering with SERVFAIL. 189 190 :param qinfo: query_info struct; 191 :param qstate: module qstate. If not None the relevant opt_lists are 192 available here; 193 :param rep: reply_info struct. None; 194 :param rcode: return code for the query. LDNS_RCODE_SERVFAIL; 195 :param edns: edns_data to be sent to the client side. If qstate is None 196 edns.opt_list contains the EDNS options sent from the client 197 side. It SHOULD NOT be altered; 198 :param opt_list_out: the list with the EDNS options that will be sent as a 199 reply. It can be populated with EDNS options; 200 :param region: region to allocate temporary data. Needs to be used when we 201 want to append a new option to opt_list_out. 202 :param **kwargs: Dictionary that may contain parameters added in a future 203 release. Current parameters: 204 ``repinfo``: Reply information for a communication point (comm_reply). 205 206 :return: True on success, False on failure. 207 208 For demonstration purposes we want to reply with an empty EDNS code '65003' 209 and log the IP address of the client. 210 211 """ 212 log_info("python: called back while servfail.") 213 # Append the example EDNS option 214 b = bytearray.fromhex("") 215 edns_opt_list_append(opt_list_out, 65003, b, region) 216 217 # Log the client's IP address 218 comm_reply = kwargs['repinfo'] 219 if comm_reply: 220 addr = comm_reply.addr 221 port = comm_reply.port 222 addr_family = comm_reply.family 223 log_info("python: Client IP: {}({}), port: {}" 224 "".format(addr, addr_family, port)) 225 226 return True 227 228 229def inplace_query_callback(qinfo, flags, qstate, addr, zone, region, **kwargs): 230 """ 231 Function that will be registered as an inplace callback function. 232 It will be called before sending a query to a backend server. 233 234 :param qinfo: query_info struct; 235 :param flags: flags of the query; 236 :param qstate: module qstate. opt_lists are available here; 237 :param addr: struct sockaddr_storage. Address of the backend server; 238 :param zone: zone name in binary; 239 :param region: region to allocate temporary data. Needs to be used when we 240 want to append a new option to opt_lists. 241 :param **kwargs: Dictionary that may contain parameters added in a future 242 release. 243 """ 244 log_info("python: outgoing query to {}@{}".format(addr.addr, addr.port)) 245 return True 246 247 248def inplace_query_response_callback(qstate, response, **kwargs): 249 """ 250 Function that will be registered as an inplace callback function. 251 It will be called after receiving a reply from a backend server. 252 253 :param qstate: module qstate. opt_lists are available here; 254 :param response: struct dns_msg. The reply received from the backend server; 255 :param **kwargs: Dictionary that may contain parameters added in a future 256 release. 257 """ 258 log_dns_msg( 259 "python: incoming reply from {}{}".format(qstate.reply.addr, os.linesep), 260 response.qinfo, response.rep 261 ) 262 return True 263 264 265def inplace_edns_back_parsed_call(qstate, **kwargs): 266 """ 267 Function that will be registered as an inplace callback function. 268 It will be called after EDNS is parsed on a reply from a backend server.. 269 270 :param qstate: module qstate. opt_lists are available here; 271 :param **kwargs: Dictionary that may contain parameters added in a future 272 release. 273 """ 274 log_info("python: edns parsed") 275 return True 276 277 278def init_standard(id, env): 279 """ 280 New version of the init function. 281 282 The function's signature is the same as the C counterpart and allows for 283 extra functionality during init. 284 285 ..note:: This function is preferred by unbound over the old init function. 286 ..note:: The previously accessible configuration options can now be found in 287 env.cfg. 288 289 """ 290 log_info("python: inited script {}".format(mod_env['script'])) 291 292 # Register the inplace_reply_callback function as an inplace callback 293 # function when answering a resolved query. 294 if not register_inplace_cb_reply(inplace_reply_callback, env, id): 295 return False 296 297 # Register the inplace_cache_callback function as an inplace callback 298 # function when answering from cache. 299 if not register_inplace_cb_reply_cache(inplace_cache_callback, env, id): 300 return False 301 302 # Register the inplace_local_callback function as an inplace callback 303 # function when answering from local data. 304 if not register_inplace_cb_reply_local(inplace_local_callback, env, id): 305 return False 306 307 # Register the inplace_servfail_callback function as an inplace callback 308 # function when answering with SERVFAIL. 309 if not register_inplace_cb_reply_servfail(inplace_servfail_callback, env, id): 310 return False 311 312 # Register the inplace_query_callback function as an inplace callback 313 # before sending a query to a backend server. 314 if not register_inplace_cb_query(inplace_query_callback, env, id): 315 return False 316 317 # Register the inplace_edns_back_parsed_call function as an inplace callback 318 # for when a reply is received from a backend server. 319 if not register_inplace_cb_query_response(inplace_query_response_callback, env, id): 320 return False 321 322 # Register the inplace_edns_back_parsed_call function as an inplace callback 323 # for when EDNS is parsed on a reply from a backend server. 324 if not register_inplace_cb_edns_back_parsed_call(inplace_edns_back_parsed_call, env, id): 325 return False 326 327 return True 328 329 330def init(id, cfg): 331 """ 332 Previous version of the init function. 333 334 ..note:: This function is still supported for backwards compatibility when 335 the init_standard function is missing. When init_standard is 336 present this function SHOULD be omitted to avoid confusion to the 337 reader. 338 339 """ 340 return True 341 342 343def deinit(id): return True 344 345 346def inform_super(id, qstate, superqstate, qdata): return True 347 348 349def operate(id, event, qstate, qdata): 350 if (event == MODULE_EVENT_NEW) or (event == MODULE_EVENT_PASS): 351 qstate.ext_state[id] = MODULE_WAIT_MODULE 352 return True 353 354 elif event == MODULE_EVENT_MODDONE: 355 qstate.ext_state[id] = MODULE_FINISHED 356 return True 357 358 log_err("pythonmod: Unknown event") 359 qstate.ext_state[id] = MODULE_ERROR 360 return True 361