| 1 | # ldd - DNS implementation in Python |
| 2 | # Copyright (C) 2006 Fredrik Tolf <fredrik@dolda2000.com> |
| 3 | # |
| 4 | # This program is free software; you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by |
| 6 | # the Free Software Foundation; either version 2 of the License, or |
| 7 | # (at your option) any later version. |
| 8 | # |
| 9 | # This program is distributed in the hope that it will be useful, |
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | # GNU General Public License for more details. |
| 13 | # |
| 14 | # You should have received a copy of the GNU General Public License |
| 15 | # along with this program; if not, write to the Free Software |
| 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 17 | |
| 18 | import bsddb |
| 19 | import threading |
| 20 | import pickle |
| 21 | import logging |
| 22 | |
| 23 | import server |
| 24 | import proto |
| 25 | import rec |
| 26 | import dn |
| 27 | |
| 28 | logger = logging.getLogger("ldd.dbzone") |
| 29 | |
| 30 | class dnsdb: |
| 31 | def __init__(self, dbdir, dbfile): |
| 32 | self.env = bsddb.db.DBEnv() |
| 33 | self.env.open(dbdir, bsddb.db.DB_JOINENV | bsddb.db.DB_THREAD) |
| 34 | self.db = bsddb.db.DB() |
| 35 | self.db.open(dbdir + "/" + dbfile, flags = bsddb.db.DB_THREAD) |
| 36 | |
| 37 | def create(self, dbdir, dbfile): |
| 38 | env = bsddb.db.DBEnv() |
| 39 | env.open(dbdir, bsddb.db.DB_CREATE | bsddb.db.DB_EXCL | bsddb.db.DB_INIT_MPOOL | bsddb.db.DB_INIT_CDB | bsddb.db.DB_THREAD) |
| 40 | db = bsddb.db.DB() |
| 41 | db.open(dbdir + "/" + dbfile, dbtype = bsddb.db.DB_HASH, flags = bsddb.db.DB_CREATE | bsddb.db.DB_EXCL | bsddb.db.DB_THREAD) |
| 42 | db.close() |
| 43 | env.close() |
| 44 | create = classmethod(create) |
| 45 | |
| 46 | def close(self): |
| 47 | self.db.close() |
| 48 | self.env.close() |
| 49 | |
| 50 | def decoderecord(self, name, record): |
| 51 | set = pickle.loads(record) |
| 52 | rrset = [] |
| 53 | for cur in set: |
| 54 | head = rec.rrhead(name, cur[0]) |
| 55 | data = cur[2] |
| 56 | newrr = rec.rr(head, cur[1], data) |
| 57 | newrr.setflags(cur[3]) |
| 58 | rrset += [newrr] |
| 59 | return rrset |
| 60 | |
| 61 | def encoderecord(self, rrset): |
| 62 | set = [] |
| 63 | for rr in rrset: |
| 64 | set += [(rr.head.rtype, rr.ttl, rr.data, rr.flags)] |
| 65 | return pickle.dumps(set) |
| 66 | |
| 67 | def lookup(self, name): |
| 68 | record = self.db.get(str(name)) |
| 69 | if record is None: |
| 70 | return None |
| 71 | return self.decoderecord(name, record) |
| 72 | |
| 73 | def set(self, name, rrset): |
| 74 | self.db.put(str(name), self.encoderecord(rrset)) |
| 75 | return True |
| 76 | |
| 77 | def hasname(self, name): |
| 78 | record = self.db.get(str(name)) |
| 79 | return record is not None |
| 80 | |
| 81 | def rmname(self, name): |
| 82 | try: |
| 83 | self.db.delete(str(name)) |
| 84 | except bsddb.db.DBNotFoundError: |
| 85 | return False |
| 86 | return True |
| 87 | |
| 88 | def rmrtype(self, name, rtype): |
| 89 | if type(rtype) == str: |
| 90 | rtype = rec.rtypebyname(rtype) |
| 91 | rrset = self.lookup(name) |
| 92 | if rrset is None: |
| 93 | return False |
| 94 | for rr in rrset: |
| 95 | if rr.head.rtype == rtype: |
| 96 | rrset.remove(rr) |
| 97 | self.set(name, rrset) |
| 98 | return True |
| 99 | |
| 100 | def addrr(self, name, rr): |
| 101 | rrset = self.lookup(name) |
| 102 | if rrset is None: |
| 103 | rrset = [] |
| 104 | rrset += [rr] |
| 105 | self.set(name, rrset) |
| 106 | return True |
| 107 | |
| 108 | def listnames(self): |
| 109 | cursor = self.db.cursor() |
| 110 | ret = cursor.first() |
| 111 | if ret is not None: |
| 112 | name, record = ret |
| 113 | yield name |
| 114 | while True: |
| 115 | ret = cursor.next() |
| 116 | if ret is None: |
| 117 | break |
| 118 | name, record = ret |
| 119 | yield name |
| 120 | cursor.close() |
| 121 | |
| 122 | def rootify(rrset, origin): |
| 123 | for rr in rrset: |
| 124 | if not rr.head.name.rooted: |
| 125 | rr.head.name += origin |
| 126 | for dname, dval in rr.data.rdata.items(): |
| 127 | if isinstance(dval, dn.domainname) and not dval.rooted: |
| 128 | rr.data.rdata[dname] += origin |
| 129 | |
| 130 | class dbhandler(server.handler): |
| 131 | def __init__(self, dbdir, dbfile): |
| 132 | self.db = dnsdb(dbdir, dbfile) |
| 133 | self.doddns = False |
| 134 | self.authkeys = [] |
| 135 | |
| 136 | def handle(self, query, pkt, origin): |
| 137 | resp = proto.responsefor(pkt) |
| 138 | if pkt.opcode == proto.QUERY: |
| 139 | rrset = self.db.lookup(query.name) |
| 140 | if rrset is None and query.name in origin: |
| 141 | rrset = self.db.lookup(query.name - origin) |
| 142 | if rrset is None: |
| 143 | return None |
| 144 | rootify(rrset, origin) |
| 145 | resp.anlist = [rr for rr in rrset if rr.head.rtype == query.rtype or rr.head.istype("CNAME")] |
| 146 | return resp |
| 147 | if pkt.opcode == proto.UPDATE: |
| 148 | logger.debug("got DDNS request") |
| 149 | if len(pkt.qlist) != 1 or not pkt.qlist[0].istype("SOA"): |
| 150 | resp.rescode = proto.FORMERR |
| 151 | return resp |
| 152 | if pkt.qlist[0].name != origin: |
| 153 | resp.rescode = proto.NOTAUTH |
| 154 | return resp |
| 155 | |
| 156 | # Check prerequisites |
| 157 | for rr in pkt.anlist: |
| 158 | if rr.ttl != 0: |
| 159 | resp.rescode = proto.FORMERR |
| 160 | return resp |
| 161 | if rr.head.name not in origin: |
| 162 | resp.rescode = proto.NOTZONE |
| 163 | return resp |
| 164 | myname = rr.head.name - origin |
| 165 | rrset = self.db.lookup(myname) |
| 166 | if rr.head.rclass == rec.CLASSANY: |
| 167 | if rr.data is not None: |
| 168 | resp.rescode = proto.FORMERR |
| 169 | return resp |
| 170 | if rr.head.rtype == proto.QTANY: |
| 171 | if rrset is None: |
| 172 | resp.rescode = proto.NXDOMAIN |
| 173 | return resp |
| 174 | else: |
| 175 | if rrset is not None: |
| 176 | for rr2 in rrset: |
| 177 | if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype: |
| 178 | break |
| 179 | else: |
| 180 | resp.rescode = proto.NXRRSET |
| 181 | return resp |
| 182 | elif rr.head.rclass == rec.CLASSNONE: |
| 183 | if rr.data is not None: |
| 184 | resp.rescode = proto.FORMERR |
| 185 | return resp |
| 186 | if rr.head.rtype == proto.QTANY: |
| 187 | if rrset is not None: |
| 188 | resp.rescode = proto.YXDOMAIN |
| 189 | return resp |
| 190 | else: |
| 191 | if rrset is not None: |
| 192 | for rr2 in rrset: |
| 193 | if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype: |
| 194 | resp.rescode = proto.YXRRSET |
| 195 | return resp |
| 196 | elif rr.head.rclass == rec.CLASSIN: |
| 197 | if rrset is not None: |
| 198 | for rr2 in rrset: |
| 199 | if rr2.head.name == myname and rr.head.rtype == rr2.head.rtype and rr.data == rr2.data: |
| 200 | break |
| 201 | else: |
| 202 | resp.rescode = proto.NXRRSET |
| 203 | return resp |
| 204 | else: |
| 205 | resp.rescode = FORMERR |
| 206 | return resp |
| 207 | |
| 208 | # Check for permission |
| 209 | if not self.doddns: |
| 210 | resp.rescode = proto.REFUSED |
| 211 | return resp |
| 212 | if type(self.authkeys) == list: |
| 213 | if pkt.tsigctx is None: |
| 214 | resp.rescode = proto.REFUSED |
| 215 | return resp |
| 216 | if pkt.tsigctx.error != 0: |
| 217 | resp.rescode = proto.NOTAUTH |
| 218 | return resp |
| 219 | if pkt.tsigctx.key not in self.authkeys: |
| 220 | resp.rescode = proto.REFUSED |
| 221 | return resp |
| 222 | elif type(self.authkeys) == None: |
| 223 | authorized = True |
| 224 | |
| 225 | # Do precheck on updates |
| 226 | for rr in pkt.aulist: |
| 227 | if rr.head.name not in origin: |
| 228 | resp.rescode = proto.NOTZONE |
| 229 | return resp |
| 230 | if rr.head.rclass == rec.CLASSIN: |
| 231 | if rr.head.rtype == proto.QTANY or rr.data is None: |
| 232 | resp.rescode = proto.FORMERR |
| 233 | return resp |
| 234 | elif rr.head.rclass == rec.CLASSANY: |
| 235 | if rr.data is not None: |
| 236 | resp.rescode = proto.FORMERR |
| 237 | return resp |
| 238 | elif rr.head.rclass == rec.CLASSNONE: |
| 239 | if rr.head.rtype == proto.QTANY or rr.ttl != 0 or rr.data is None: |
| 240 | resp.rescode = proto.FORMERR |
| 241 | return resp |
| 242 | else: |
| 243 | resp.rescode = proto.FORMERR |
| 244 | return resp |
| 245 | |
| 246 | # Perform updates |
| 247 | for rr in pkt.aulist: |
| 248 | myname = rr.head.name - origin |
| 249 | if rr.head.rclass == rec.CLASSIN: |
| 250 | logger.info("adding rr (%s)", rr) |
| 251 | self.db.addrr(myname, rr) |
| 252 | elif rr.head.rclass == rec.CLASSANY: |
| 253 | if rr.head.rtype == proto.QTANY: |
| 254 | logger.info("removing rrset (%s)", rr.head.name) |
| 255 | self.db.rmname(myname) |
| 256 | else: |
| 257 | logger.info("removing rrset (%s, %s)", rr.head.name, rr.head.rtype) |
| 258 | self.db.rmrtype(myname, rr.head.rtype) |
| 259 | elif rr.head.rclass == rec.CLASSNONE: |
| 260 | logger.info("removing rr (%s)", rr) |
| 261 | rrset = self.db.lookup(myname) |
| 262 | changed = False |
| 263 | if rrset is not None: |
| 264 | for rr2 in rrset: |
| 265 | if rr2.head == rr.head and rr2.data == rr.data: |
| 266 | rrset.remove(rr2) |
| 267 | changed = True |
| 268 | self.db.set(myname, rrset) |
| 269 | |
| 270 | return resp |
| 271 | |
| 272 | resp.rescode = proto.NOTIMP |
| 273 | return resp |