1e2b1b9c0Schristos# Copyright (C) Internet Systems Consortium, Inc. ("ISC") 2e2b1b9c0Schristos# 3*497bf0b8Schristos# SPDX-License-Identifier: MPL-2.0 4*497bf0b8Schristos# 5e2b1b9c0Schristos# This Source Code Form is subject to the terms of the Mozilla Public 6e2b1b9c0Schristos# License, v. 2.0. If a copy of the MPL was not distributed with this 78260f9a8Schristos# file, you can obtain one at https://mozilla.org/MPL/2.0/. 8e2b1b9c0Schristos# 9e2b1b9c0Schristos# See the COPYRIGHT file distributed with this work for additional 10e2b1b9c0Schristos# information regarding copyright ownership. 11e2b1b9c0Schristos 12e2b1b9c0Schristos############################################################################ 13e2b1b9c0Schristos# rndc.py 14e2b1b9c0Schristos# This module implements the RNDC control protocol. 15e2b1b9c0Schristos############################################################################ 16e2b1b9c0Schristos 17e2b1b9c0Schristosfrom collections import OrderedDict 18e2b1b9c0Schristosimport time 19e2b1b9c0Schristosimport struct 20e2b1b9c0Schristosimport hashlib 21e2b1b9c0Schristosimport hmac 22e2b1b9c0Schristosimport base64 23e2b1b9c0Schristosimport random 24e2b1b9c0Schristosimport socket 25e2b1b9c0Schristos 26e2b1b9c0Schristos 27e2b1b9c0Schristosclass rndc(object): 28e2b1b9c0Schristos """RNDC protocol client library""" 29*497bf0b8Schristos 30*497bf0b8Schristos __algos = { 31*497bf0b8Schristos "md5": 157, 32*497bf0b8Schristos "sha1": 161, 33*497bf0b8Schristos "sha224": 162, 34*497bf0b8Schristos "sha256": 163, 35*497bf0b8Schristos "sha384": 164, 36*497bf0b8Schristos "sha512": 165, 37*497bf0b8Schristos } 38e2b1b9c0Schristos 39e2b1b9c0Schristos def __init__(self, host, algo, secret): 40e2b1b9c0Schristos """Creates a persistent connection to RNDC and logs in 41e2b1b9c0Schristos host - (ip, port) tuple 42e2b1b9c0Schristos algo - HMAC algorithm: one of md5, sha1, sha224, sha256, sha384, sha512 43e2b1b9c0Schristos (with optional prefix 'hmac-') 44e2b1b9c0Schristos secret - HMAC secret, base64 encoded""" 45e2b1b9c0Schristos self.host = host 46e2b1b9c0Schristos algo = algo.lower() 47*497bf0b8Schristos if algo.startswith("hmac-"): 48e2b1b9c0Schristos algo = algo[5:] 49e2b1b9c0Schristos self.algo = algo 50e2b1b9c0Schristos self.hlalgo = getattr(hashlib, algo) 51e2b1b9c0Schristos self.secret = base64.b64decode(secret) 52e2b1b9c0Schristos self.ser = random.randint(0, 1 << 24) 53e2b1b9c0Schristos self.nonce = None 54e2b1b9c0Schristos self.__connect_login() 55e2b1b9c0Schristos 56e2b1b9c0Schristos def call(self, cmd): 57e2b1b9c0Schristos """Call a RNDC command, all parsing is done on the server side 58e2b1b9c0Schristos cmd - a complete string with a command (eg 'reload zone example.com') 59e2b1b9c0Schristos """ 60*497bf0b8Schristos return dict(self.__command(type=cmd)["_data"]) 61e2b1b9c0Schristos 62e2b1b9c0Schristos def __serialize_dict(self, data, ignore_auth=False): 63e2b1b9c0Schristos rv = bytearray() 64e2b1b9c0Schristos for k, v in data.items(): 65*497bf0b8Schristos if ignore_auth and k == "_auth": 66e2b1b9c0Schristos continue 67*497bf0b8Schristos rv += struct.pack("B", len(k)) + k.encode("ascii") 68e2b1b9c0Schristos if type(v) == str: 69*497bf0b8Schristos rv += struct.pack(">BI", 1, len(v)) + v.encode("ascii") 70e2b1b9c0Schristos elif type(v) == bytes: 71*497bf0b8Schristos rv += struct.pack(">BI", 1, len(v)) + v 72e2b1b9c0Schristos elif type(v) == bytearray: 73*497bf0b8Schristos rv += struct.pack(">BI", 1, len(v)) + v 74e2b1b9c0Schristos elif type(v) == OrderedDict: 75e2b1b9c0Schristos sd = self.__serialize_dict(v) 76*497bf0b8Schristos rv += struct.pack(">BI", 2, len(sd)) + sd 77e2b1b9c0Schristos else: 78*497bf0b8Schristos raise NotImplementedError( 79*497bf0b8Schristos "Cannot serialize element of type %s" % type(v) 80*497bf0b8Schristos ) 81e2b1b9c0Schristos return rv 82e2b1b9c0Schristos 83e2b1b9c0Schristos def __prep_message(self, *args, **kwargs): 84e2b1b9c0Schristos self.ser += 1 85e2b1b9c0Schristos now = int(time.time()) 86e2b1b9c0Schristos data = OrderedDict(*args, **kwargs) 87e2b1b9c0Schristos 88e2b1b9c0Schristos d = OrderedDict() 89*497bf0b8Schristos d["_auth"] = OrderedDict() 90*497bf0b8Schristos d["_ctrl"] = OrderedDict() 91*497bf0b8Schristos d["_ctrl"]["_ser"] = str(self.ser) 92*497bf0b8Schristos d["_ctrl"]["_tim"] = str(now) 93*497bf0b8Schristos d["_ctrl"]["_exp"] = str(now + 60) 94e2b1b9c0Schristos if self.nonce is not None: 95*497bf0b8Schristos d["_ctrl"]["_nonce"] = self.nonce 96*497bf0b8Schristos d["_data"] = data 97e2b1b9c0Schristos 98e2b1b9c0Schristos msg = self.__serialize_dict(d, ignore_auth=True) 99e2b1b9c0Schristos hash = hmac.new(self.secret, msg, self.hlalgo).digest() 100e2b1b9c0Schristos bhash = base64.b64encode(hash) 101*497bf0b8Schristos if self.algo == "md5": 102*497bf0b8Schristos d["_auth"]["hmd5"] = struct.pack("22s", bhash) 103e2b1b9c0Schristos else: 104*497bf0b8Schristos d["_auth"]["hsha"] = bytearray( 105*497bf0b8Schristos struct.pack("B88s", self.__algos[self.algo], bhash) 106*497bf0b8Schristos ) 107e2b1b9c0Schristos msg = self.__serialize_dict(d) 108*497bf0b8Schristos msg = struct.pack(">II", len(msg) + 4, 1) + msg 109e2b1b9c0Schristos return msg 110e2b1b9c0Schristos 111e2b1b9c0Schristos def __verify_msg(self, msg): 112*497bf0b8Schristos if self.nonce is not None and msg["_ctrl"]["_nonce"] != self.nonce: 113e2b1b9c0Schristos return False 114*497bf0b8Schristos if self.algo == "md5": 115*497bf0b8Schristos bhash = msg["_auth"]["hmd5"] 116e2b1b9c0Schristos else: 117*497bf0b8Schristos bhash = msg["_auth"]["hsha"][1:] 118e2b1b9c0Schristos if type(bhash) == bytes: 119*497bf0b8Schristos bhash = bhash.decode("ascii") 120*497bf0b8Schristos bhash += "=" * (4 - (len(bhash) % 4)) 121e2b1b9c0Schristos remote_hash = base64.b64decode(bhash) 122e2b1b9c0Schristos my_msg = self.__serialize_dict(msg, ignore_auth=True) 123e2b1b9c0Schristos my_hash = hmac.new(self.secret, my_msg, self.hlalgo).digest() 124*497bf0b8Schristos return my_hash == remote_hash 125e2b1b9c0Schristos 126e2b1b9c0Schristos def __command(self, *args, **kwargs): 127e2b1b9c0Schristos msg = self.__prep_message(*args, **kwargs) 128e2b1b9c0Schristos sent = self.socket.send(msg) 129e2b1b9c0Schristos if sent != len(msg): 130e2b1b9c0Schristos raise IOError("Cannot send the message") 131e2b1b9c0Schristos 132e2b1b9c0Schristos header = self.socket.recv(8) 133e2b1b9c0Schristos if len(header) != 8: 134e2b1b9c0Schristos # What should we throw here? Bad auth can cause this... 135e2b1b9c0Schristos raise IOError("Can't read response header") 136e2b1b9c0Schristos 137*497bf0b8Schristos length, version = struct.unpack(">II", header) 138e2b1b9c0Schristos if version != 1: 139*497bf0b8Schristos raise NotImplementedError("Wrong message version %d" % version) 140e2b1b9c0Schristos 141e2b1b9c0Schristos # it includes the header 142e2b1b9c0Schristos length -= 4 143e2b1b9c0Schristos data = self.socket.recv(length, socket.MSG_WAITALL) 144e2b1b9c0Schristos if len(data) != length: 145e2b1b9c0Schristos raise IOError("Can't read response data") 146e2b1b9c0Schristos 147e2b1b9c0Schristos if type(data) == str: 148e2b1b9c0Schristos data = bytearray(data) 149e2b1b9c0Schristos msg = self.__parse_message(data) 150e2b1b9c0Schristos if not self.__verify_msg(msg): 151e2b1b9c0Schristos raise IOError("Authentication failure") 152e2b1b9c0Schristos 153e2b1b9c0Schristos return msg 154e2b1b9c0Schristos 155e2b1b9c0Schristos def __connect_login(self): 156e2b1b9c0Schristos self.socket = socket.create_connection(self.host) 157e2b1b9c0Schristos self.nonce = None 158*497bf0b8Schristos msg = self.__command(type="null") 159*497bf0b8Schristos self.nonce = msg["_ctrl"]["_nonce"] 160e2b1b9c0Schristos 161e2b1b9c0Schristos def __parse_element(self, input): 162e2b1b9c0Schristos pos = 0 163e2b1b9c0Schristos labellen = input[pos] 164e2b1b9c0Schristos pos += 1 165*497bf0b8Schristos label = input[pos : pos + labellen].decode("ascii") 166e2b1b9c0Schristos pos += labellen 167e2b1b9c0Schristos type = input[pos] 168e2b1b9c0Schristos pos += 1 169*497bf0b8Schristos datalen = struct.unpack(">I", input[pos : pos + 4])[0] 170e2b1b9c0Schristos pos += 4 171e2b1b9c0Schristos data = input[pos : pos + datalen] 172e2b1b9c0Schristos pos += datalen 173e2b1b9c0Schristos rest = input[pos:] 174e2b1b9c0Schristos 175e2b1b9c0Schristos if type == 1: # raw binary value 176e2b1b9c0Schristos return label, data, rest 177e2b1b9c0Schristos elif type == 2: # dictionary 178e2b1b9c0Schristos d = OrderedDict() 179e2b1b9c0Schristos while len(data) > 0: 180e2b1b9c0Schristos ilabel, value, data = self.__parse_element(data) 181e2b1b9c0Schristos d[ilabel] = value 182e2b1b9c0Schristos return label, d, rest 183e2b1b9c0Schristos # TODO type 3 - list 184e2b1b9c0Schristos else: 185*497bf0b8Schristos raise NotImplementedError("Unknown element type %d" % type) 186e2b1b9c0Schristos 187e2b1b9c0Schristos def __parse_message(self, input): 188e2b1b9c0Schristos rv = OrderedDict() 189e2b1b9c0Schristos hdata = None 190e2b1b9c0Schristos while len(input) > 0: 191e2b1b9c0Schristos label, value, input = self.__parse_element(input) 192e2b1b9c0Schristos rv[label] = value 193e2b1b9c0Schristos return rv 194