#!/usr/bin/env python # Read a tinydns data file, and output BIND-style zone files and config. # Adam Sampson # # The input language this understands is documented on the djbdns web site: # http://cr.yp.to/djbdns/tinydns-data.html # It only generates zone files for domains that it's seen an SOA entry for, so # if you're not getting any output make sure you've got the appropriate "." # lines for your domains in the input file. # # I've used the output from this script successfully with PowerDNS and nsd, but # the zone files should work with most nameservers. import sys, os, os.path def die(*s): sys.stderr.write(" ".join(map(str, s)) + "\n") sys.exit(1) def make_inaddr(ip): bits = ip.split(".") if len(bits) != 4: die("Bad IPv4 address:", ip) bits.reverse() return ".".join(bits) + ".in-addr.arpa" def add_dot(s): if s != "" and s[-1] != ".": return s + "." else: return s def unescape_tinydns(s): out = "" i = 0 while i < len(s): if s[i] == '\\': out += chr(int(s[i + 1:i + 4], 8)) i += 3 else: out += s[i] i += 1 return out def escape_zone(s): return s.replace('"', '\"') def main(args): if len(args) != 2: die("Usage: tinydns-to-zones DATA-FILE OUTPUT-DIR") (input_fn, output_dir) = args domains = {} records = [] f = open(input_fn, "r") default_ser = str(int(os.fstat(f.fileno()).st_mtime)) for l in f.xreadlines(): l = l.strip() if l == "": continue c = l[0] if c == "#" or c == "-": continue a = l[1:].split(":") ttl = "" timestamp = "" lo = "" def parse(n): global ttl, timestamp, lo if len(a) < 2: die("Too few args in line:", l) while len(a) < n: a.append("") (ttl, timestamp, lo) = (a[n:] + ["", "", ""])[:3] if timestamp != "" or lo != "": die("timestamp/lo not yet supported in line:", l) return a[:n] def add_record(fqdn, type, data, def_ttl = "86400"): global ttl if ttl == "": ttl = def_ttl records.append([add_dot(fqdn), "IN", ttl, type, data]) def add_soa(fqdn, mname, rname, ser = default_ser, ref = "", ret = "", exp = "", min = ""): if ser == "": ser = default_ser if ref == "": ref = "16384" if ret == "": ret = "2048" if exp == "": exp = "1048576" if min == "": min = "2560" global ttl if ttl == "": ttl = "2560" domains[fqdn] = [add_dot(fqdn), "IN", ttl, "SOA", "%s %s %s %s %s %s %s" % (add_dot(mname), add_dot(rname), ser, ref, ret, exp, min)] if c == "." or c == "&": (fqdn, ip, x) = parse(3) if "." in x: ns_name = x else: ns_name = x + ".ns." + fqdn if c == ".": add_soa(fqdn, ns_name, "hostmaster." + fqdn) add_record(fqdn, "NS", add_dot(ns_name), "2560") if ip != "": add_record(ns_name, "A", ip) elif c == "=" or c == "+": (fqdn, ip) = parse(2) add_record(fqdn, "A", ip) if c == "=": add_record(make_inaddr(ip), "PTR", add_dot(fqdn)) elif c == "@": (fqdn, ip, x, dist) = parse(4) if "." in x: mx_name = x else: mx_name = x + ".mx." + fqdn if dist == "": dist = "0" add_record(fqdn, "MX", dist + " " + add_dot(mx_name)) if ip != "": # not in spec, but assumed... add_record(mx_name, "A", ip) elif c == "'": (fqdn, s) = parse(2) s = escape_zone(unescape_tinydns(s)) add_record(fqdn, "TXT", '"' + s + '"') elif c == "^": (fqdn, p) = parse(2) add_record(fqdn, "PTR", add_dot(p)) elif c == "C": (fqdn, p) = parse(2) add_record(fqdn, "CNAME", add_dot(p)) elif c == "Z": a = parse(8) add_soa(*a) elif c == ":": (fqdn, n, rdata) = parse(3) if int(n) != 16: die("Generic record not supported in line:", l) # This is the specific case of : being used to generate # a TXT record, to work around tinydns splitting ' data # into multiple items. This is widely used for # DomainKeys. rdata = unescape_tinydns(rdata) data = "" while rdata != "": bytes = ord(rdata[0]) data += rdata[1:bytes + 1] rdata = rdata[bytes + 1:] add_record(fqdn, "TXT", '"' + escape_zone(data) + '"') f.close() def write_record(f, r): f.write("\t".join(r) + "\n") output_dir = os.path.abspath(output_dir) try: os.makedirs(output_dir + "/zones") except OSError: pass cf = open(output_dir + "/named.conf", "w") nf = open(output_dir + "/nsd.zones", "w") for dom in domains.keys(): soa = domains[dom] fn = "zones/" + dom + ".pri" # Use fully-qualified path in the config file, since PowerDNS # will complain otherwise... full_fn = output_dir + "/" + fn cf.write('zone "' + dom + '" in {\n type master;\n file "' + full_fn + '";\n};\n') nf.write('zone\t%s\t%s\n' % (dom, fn)) f = open(full_fn, "w") write_record(f, soa) for r in records: if r[0].endswith(soa[0]): write_record(f, r) f.close() cf.close() nf.close() cf.close() if __name__ == "__main__": main(sys.argv[1:])