#!/usr/bin/env python3 ''' Usage: zone_generate.py [parameters] origin Parameters: -h, --help This help. -s, --sign Sign the zone with dnssec-signzone. -3, --nsec3=y|n Use/don't use NSEC3. If not specified choose randomly. -i, --serial=num Specify SOA serial. -u, --update=file Update zone file (no extra SOA). -n, --names=num Generate unique zone names. -t, --ttl=sec Specify default TTL. -o, --outfile=file Specify output file name. -k, --keydir=dir Specify output key directory. -c, --count=num RR count ''' import binascii import getopt import string import os import random import re import shutil import subprocess import sys import tempfile import dns import dns.rdataclass import dns.rdatatype import dns.rdata class Context(object): # Zone name ORIGIN = 'com' # Domain for reverse zones RORIGIN = None # Zone is RPREFIX classless-type RPREFIX = None # 0.0 - 1.0, chance of dname bing a subdomain SUB_CHANCE = 0.0 # 0.0 - 1.0, percentage of mangled words WORD_MRATE = 1.0 # 0.0 - 1.0, chance for FQDN FQDN_CHANCE = 0.5 ctx = Context() RRTYPES = [ \ # [ typename, generator, probability, typeid ] [ 'A', 'g_a', 1.00, 1 ], \ [ 'NS', 'g_ns', 0.25, 2 ], \ # [ 'PTR', 'g_ptr', 0.50, 12 ], \ [ 'HINFO', 'g_hinfo', 0.05, 13 ], \ [ 'MX', 'g_mx', 0.25, 15 ], \ [ 'TXT', 'g_txt', 0.15, 16 ], \ [ 'RP', 'g_rp', 0.02, 17 ], \ [ 'AFSDB', 'g_mx', 0.02, 18 ], \ [ 'RT', 'g_mx', 0.02, 21 ], \ [ 'AAAA', 'g_aaaa', 1.00, 28 ], \ [ 'LOC', 'g_loc', 0.15, 29 ], \ [ 'SRV', 'g_srv', 0.25, 33 ], \ [ 'KX', 'g_mx', 0.02, 36 ], \ [ 'CERT', 'g_cert', 0.05, 37 ], \ [ 'APL', 'g_apl', 0.05, 42 ], \ [ 'SSHFP', 'g_sshfp', 0.10, 44 ], \ [ 'IPSECKEY', 'g_ipseckey', 0.05, 45 ], \ # [ 'DNSKEY', 'g_key', 0.01, 48 ], \ [ 'DHCID', 'g_dhcid', 0.01, 49 ], \ [ 'SPF', 'g_spf', 0.15, 99 ], \ [ 'CUSTOM', 'g_customrr', 0.03, 0 ] \ ] WORDS = [ "citadel", "virmire", "feros", "noveria", "geth", "reapers", "normandy", "cerberus", "aperture","weight","companion","science","cube","enrichment","glados","center", "harbinger", "collector", "assuming-control", "intervention", "necessary", "test","dev","ads","adserver","adsl","agent","channel","dmz","sz","client","imap","http", "argos", "kepler", "armstrong", "nebula", "artemis", "hades", "nexus", "hawking", "titan", "aldrinlabs", "devlon", "elanus", "elkoss", "hahnekedar", "haliat", "rosenkov", "sirta", "triumph","cake","party","portal","gun","fault","alive","environment","advice", "work","day","person","material","emancipation","grid","subject","test","mass","velocity", "linux","windows","log","install","blog","host","printer","public","sql","mysql","router", "protocol","area","fabric","term","case","fluid","catalyst","unit","maintenance","android", "cisco","switch","telnet","voip","webmin","ssh","delevlop","pub","user","xml", "telnet","extern","intranet","extranet","testing","default","gateway","radius","noc", "mobile","customer","siprouter","sip","office","voice","support", "spare","owa","exchange" ] # Replace some words with random ones for i, word in enumerate(WORDS): if random.choice([True, False]): size = random.randint(2, 20) WORDS[i] = ''.join(random.choice(string.hexdigits) for _ in range(size)) # For delegations DELEG_EXIST = set([]) # For A/AAAA names A_NAMES = [] AAAA_NAMES = [] # Generate random number def rnd(a, b): return random.randint(a, b) def rnd_fl(a, b): return random.uniform(a, b) def choice(items): return random.choice(items) def rnd_hex(l): return '%x' % random.randrange(int(256**(l/2))) def rnd_str(): i = rnd(0, len(WORDS)-1) word = WORDS[i] if rnd_fl(0, 1) < ctx.WORD_MRATE: i = rnd(1, len(word)) word = word[0:i] + rnd_hex(rnd(2,8)) return word def rnd_dname(enable_sub = 1): dname = rnd_str() # Chance for subdomain if enable_sub == 1 and rnd_fl(0, 1) < ctx.SUB_CHANCE: dname += '.%s' % rnd_dnl(0) # Ensure owners not below delegations # Chance for FQDN if rnd_fl(0, 1) < ctx.FQDN_CHANCE: dname = g_fqdn(dname) return dname def rnd_dnl(enable_sub = 1): dn = rnd_dname(enable_sub) fqdn = g_fqdn(dn) while fqdn.lower() in DELEG_EXIST: dn = rnd_dname(enable_sub) fqdn = g_fqdn(dn) return dn def rnd_dnr(): src = choice([A_NAMES, AAAA_NAMES]) if len(src) == 0: if len(A_NAMES) > 0: src = A_NAMES else: src = AAAA_NAMES if src: return choice(src) else : return rnd_dname() def rnd_ip4(): return '%d.%d.%d.%d' % (rnd(0,255), rnd(0,255), rnd(0,255), rnd(0,255)) def rnd_ip6(): # Private address range addr = 'fd9c:20c0:91fc:cb36' for _ in range(0,4): addr += ':' + rnd_hex(4) return addr def rnd_srv(): return random.choice(['sip', 'xmpp', 'ldap']) def rnd_proto(): return random.choice(['udp', 'tcp']) def shuffle_str(s): s = list(s) random.shuffle(s) return ''.join(s) def g_rdbin(data): dl = len(data) hs = binascii.hexlify(data).decode('ascii') return '\\# %d %s' % (dl, hs) def g_rdata(rt, data, chance=30): if rnd(0, 100) < chance: cls = dns.rdataclass.IN tpe = dns.rdatatype.from_text(rt[0]) rd = dns.rdata.from_text(cls, tpe, data) return g_rdbin(rd.to_digestable(dns.name.from_text(ctx.ORIGIN))) + ' ;rdata=' + data return data def g_rtype(rt): if rnd_fl(0, 100) < 70: # 70% return rt[0] return "TYPE%d" % rt[3] def g_fqdn(dn): if not dn.endswith('.'): if ctx.ORIGIN == ".": dn += '.' else: dn += '.%s.' % ctx.ORIGIN return dn def g_customrr(rt): bin = rnd_hex(rnd(10,50)).encode() return '%s TYPE%d %s' % (rnd_dnl(), rnd(300,32767), g_rdbin(bin)) # RR Generators def g_a(rt): dn = rnd_dnl(0) A_NAMES.append(dn) return '%s A %s' % (dn, rnd_ip4()) def g_aaaa(rt): dn = rnd_dnl(0) AAAA_NAMES.append(dn) return '%s AAAA %s' % (dn, rnd_ip6()) def g_srv(rt): name = '_%s._%s.%s' % (rnd_srv(), rnd_proto(), rnd_dnl()) rdt = g_rdata(rt, '%d %d %d %s' % (rnd(1, 50), rnd(1, 50), rnd(1024, 65535), rnd_dnr())) return '%s %s %s' % (name, g_rtype(rt), rdt) def g_ns(rt): dn = rnd_dname() fqdn = g_fqdn(dn) DELEG_EXIST.add(fqdn.lower()) # Value (domain-name) rd = rnd_dnr() return '%s %s %s' % (dn, g_rtype(rt), rd) def g_mx(rt): rd = rnd_dnr() return '%s %s %d %s' % (rnd_dnl(), g_rtype(rt), rnd(1, 20), rd) def g_txt(rt): sentences = "" for _ in range(1, 32): sentences += ' "%s"' % (' '.join(random.sample(WORDS, rnd(1, 5)))) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), g_rdata(rt, sentences, 10)) def g_loc(rt): rd = g_rdata(rt, '%d %d %.03f %s %d %d %.03f %s %d' % \ ( rnd(0, 89), rnd(0, 59), rnd_fl(0, 59.99), random.choice('NS'), \ rnd(0, 89), rnd(0, 59), rnd_fl(0, 59.99), random.choice('EW'), \ rnd(-100, 4000))) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), rd) def g_spf(rt): choices = [ 'ip4:%s' % rnd_ip4(), 'ip6:%s' % rnd_ip6(), \ '%s:%s' % (random.choice(['a','mx']), rnd_dname()) ] rd = g_rdata(rt, '"v=spf1 %s -all"' % random.choice(choices)) return '@ IN %s %s' % (g_rtype(rt), rd) def g_ptr(rt): return '%s %s %s' % (rnd_ip4(), g_rtype(rt), rnd_dname()) def g_hinfo(rt): pf = '%s-%dmhz' % (random.choice(['PC-Intel','ARM','PPC']), rnd(500,700)) os = '%s %d.%d' % (random.choice(['Linux', 'BSD']), rnd(1,9), rnd(1,9)) return '%s %s %s "%s"' % (rnd_dnl(), g_rtype(rt), pf, os) def g_rp(rt): # name ttl class rr email-addr return '%s %s %s %s' % \ (rnd_dnl(), g_rtype(rt), g_fqdn('admin.'+rnd_str()), g_fqdn('sysadmins.'+rnd_str())) def g_nsap(rt): # name ttl class rr nsap-address # TODO: Dynamic address return '%s %s %s' % (rnd_dnl(), g_rtype(rt), \ g_rdata(rt, '0x%s' % '47.0005.80.005a00.0000.0001.e133.ffffff000161.00')) def g_dhcid(rt): # TODO: bogus data, described in RFC4701 as DNS Binary RR format return '%s %s %s' % \ (rnd_dnl(), g_rtype(rt), g_rdata(rt, 'VGhpcyBzaG9ydCBzbmlwcGV0IG9mIHRleHQgaXMgc2FkIGFuZCBtZWFuaW5nbGVzcy4K')) def g_cert(rt): # name ttl class rr type key-tag algorithm cert-crl # TODO: dnssec-keygen generated values (slow?) # TODO: values from book Pro DNS and BIND 10 rd = g_rdata(rt, '%d 12179 3 %s' % \ (rnd(1,8), 'VGhpcyBzaG9ydCBzbmlwcGV0IG9mIHRleHQgaXMgc2FkIGFuZCBtZWFuaW5nbGVzcy4K')) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), rd) def g_key(rt): # name ttl class rr flags proto algorithm key-data # TODO: values from book Pro DNS and BIND 10 rd = g_rdata(rt, '%d 3 %d %s' % \ (256, rnd(1,5), 'VGhpcyBzaG9ydCBzbmlwcGV0IG9mIHRleHQgaXMgc2FkIGFuZCBtZWFuaW5nbGVzcy4K')) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), rd) def g_sshfp(rt): key = shuffle_str('123456789abcdef67890123456789abcdef67890') rd = g_rdata(rt, '%d %d %s' % (choice([1,2]), 1, key)) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), rd) def g_ipseckey(rt): # precedence gw-type algorithm gw pubkey # TODO: Doesn't make much sense in non-reverse zones dn = rnd_ip4() prec = rnd(1,20) gwtype = 3 #rnd(1, 3) # TODO: fix, 1,2 needs valid IPs as dnames in zone algo = rnd(1, 2) gw = '' if gwtype == 1: gw = rnd_ip4() elif gwtype == 2: gw = rnd_ip6() else: gw = rnd_dnl() pkey = 'AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ==' rd = g_rdata(rt, '%d %d %d %s %s' % (prec, gwtype, algo, gw, pkey)) return '%s IN %s %s' % (dn, g_rtype(rt), rd) def g_apl(rt): data = '' dcount = rnd(1,3) for _ in range(0, dcount): afi = choice([1, 2]) ip = '' if afi == 1: ip = rnd_ip4() else: ip = rnd_ip6() data += '%s%d:%s/%d ' % (choice(['!','']), afi, ip, rnd(2,32)) return '%s %s %s' % (rnd_dnl(), g_rtype(rt), g_rdata(rt, data)) # Generate RR def gen_rr(): rd = random.choice(RRTYPES) if rnd_fl(0,1) < rd[2]: f = globals()[rd[1]] if f is None: return None return '%s' % (f(rd)) else: return None # Generate reverse RR # @TODO non-RFC2317 reverse zones def gen_rr_rev(dn): return '%s\tPTR\t%s' % (dn, g_fqdn(rnd_dnl())) # Generate SOA def gen_soa(origin, serial, ttl, auth = None): refresh = 3600 if origin != '.': origin += '.' soa = '' soa += '$TTL %d\n' % ttl s = '@ IN SOA %s %s' % (g_fqdn('ns'), g_fqdn('username')) s += ' %s %d %d %s %s\n' % (serial, refresh, refresh / 3, '4w', ttl ) if auth != None: if auth != '.': auth += '.' soa += '$ORIGIN %s\n' % auth soa += s soa += '@ NS ns.%s\n' % origin soa += '@ NS ns2.%s\n' % origin soa += '; END OF SOA\n' else: soa += '$ORIGIN %s\n' % origin soa += s soa += '@ NS ns\n' soa += '@ NS ns2\n' soa += 'ns A %s\n' % rnd_ip4() soa += 'ns2 A %s\n' % rnd_ip4() soa += '; END OF SOA\n' return soa # Generate unique zone names GENERATED = set([]) def g_unique(): dn = rnd_dnl() while dn.lower() in GENERATED: dn = rnd_dnl() GENERATED.add(dn.lower()) return dn def g_unique_names(count): ctx.FQDN_CHANCE = 0.0 ctx.SUB_CHANCE = 0 ctx.ORIGIN = g_unique() ctx.SUB_CHANCE = 0.2 ctx.WORD_MRATE = 0.3 o = '' for _ in range(0, count): if rnd(0,1) < ctx.SUB_CHANCE: ctx.ORIGIN = rnd_dnl() o += g_unique() + ' ' if __name__ == "__main__": print(o) return 0 else: return o def main(args): serial = '2020091813' ttl = random.randint(1800, 18000) count = 100 UPDATE = None sign = 0 nsec3 = random.choice([True, 0, False]) out_fname = None key_dir = None # Parse parameters try: opts, args = getopt.getopt(args, 'hs3:i:u:n:t:o:k:c:', ['help', 'sign', 'nsec3=', 'serial=', 'update=', 'names=', 'ttl=', 'outfile=', 'keydir=', 'count=']) except getopt.error as msg: print(msg) print('for help use --help') sys.exit(2) for o, a in opts: if o in ('-h', '--help'): print(__doc__) sys.exit(0) if o in ('-s', '--sign'): sign = 1 if o in ('-3', '--nsec3'): if a == 'y': nsec3 = True elif str(a) == "0": nsec3 = 0 else: nsec3 = False if o in ('-i', '--serial') and a != None: serial = a if o in ('-u', '--update') and a != None: UPDATE = a if o in ('-n', '--names') and a != None: return g_unique_names(int(a)) if o in ('-t', '--ttl') and a != None: ttl = int(a) if o in ('-o', '--outfile') and a != None: out_fname = a if o in ('-k', '--keydir') and a != None: key_dir = a if o in ('-c', '--count') and a != None: count = int(a) # Check arguments if len(args) > 1: print('Too many arguments.') print(__doc__) sys.exit(2) # Parse non-option arguments if args: a = args[0] if a != '.': a = a.rstrip('.') ctx.ORIGIN = a # Check values if not out_fname: out_fname = UPDATE if UPDATE else ctx.ORIGIN + '.zone' # Reverse zones if ctx.ORIGIN.find('in-addr') > 0: ctx.RORIGIN = ctx.ORIGIN ctx.ORIGIN = rnd_str() pp = ctx.RORIGIN.find('/') if pp > 0: ctx.RPREFIX = int(ctx.RORIGIN[0:pp]) tmp_dir = tempfile.mkdtemp() in_fname = tmp_dir + '/zfile' # Open zone file if UPDATE: shutil.copyfile(UPDATE, in_fname) outf = open(in_fname, "a") if not UPDATE: outf.write(gen_soa(ctx.ORIGIN, serial, ttl, ctx.RORIGIN)) # Check if prefix exists if ctx.RPREFIX != None and count > ctx.RPREFIX: count = ctx.RPREFIX - 1 # <1,RPREFIX) # @TODO update reverse zone if UPDATE and ctx.RPREFIX != None: count = 0 # Now generate RRs a_pool = count / 10 if a_pool > 1000: a_pool = 1000 for i in range(0, count): # Make a batch of A/AAAAs at start rr = None if ctx.RORIGIN != None: # Reverse zone records rr = gen_rr_rev(i + 1) elif i < a_pool: if rnd(0,1) < 0.5: rr = g_a('A') else: rr = g_aaaa('AAAA') else: # Select until it generates valid RR # This crudely implements weighted distribution while rr is None: rr = gen_rr() outf.write(rr + '\n') # Return if no signing is required if sign == 0: outf.close() shutil.copyfile(in_fname, out_fname) shutil.rmtree(tmp_dir) return 0 # Now sign if requested if not key_dir: key_dir = tmp_dir ret = 1 try: # Generate keys ps = [ 'dnssec-keygen', '-n', 'ZONE', '-a', 'RSASHA256', '-b', '1024', '-K', key_dir ] if nsec3 is not False: ps += ['-3'] k1 = subprocess.check_output(ps + [ctx.ORIGIN], stderr=subprocess.DEVNULL) k2 = subprocess.check_output(ps + ["-f", "KSK"] + [ctx.ORIGIN], stderr=subprocess.DEVNULL) k1 = key_dir + '/' + k1.rstrip().decode('ascii') k2 = key_dir + '/' + k2.rstrip().decode('ascii') # Append to zone kf = open(k1 + '.key', 'r') outf.write(kf.read() + '\n') kf.close() kf = open(k2 + '.key', 'r') outf.write(kf.read() + '\n') kf.close() outf.close() if nsec3 is False: nsec3_params = [] elif nsec3 == 0: nsec3_params = ['-3', '-'] else: nsec3_params = ['-3', binascii.hexlify(os.urandom(random.randint(1, 30))).decode('ascii')] subprocess.check_output(["dnssec-signzone", "-d", tmp_dir, "-P", "-u", \ "-k", k2, "-x", "-o", ctx.ORIGIN, \ "-O", "full"] + nsec3_params + [in_fname, k1 + ".key"], stderr=subprocess.DEVNULL) shutil.copyfile(in_fname + '.signed', out_fname) ret = 0 except: pass else: shutil.rmtree(tmp_dir) return ret if __name__ == "__main__": sys.exit(main(sys.argv[1:]))