From 6cb57c6dbfdc4fa574b30243a142e6b8cd6bea09 Mon Sep 17 00:00:00 2001 From: Gilles Grandou Date: Sat, 3 Oct 2020 23:18:00 +0200 Subject: [PATCH] big update * zone.list moved to zone.json * entries are deleted from zone only after a grace period * preparation for firewall support --- .gitignore | 4 +- dyndomain | 291 ++++++++++++++++++++++++++++------------------- home-sample.conf | 16 +-- 3 files changed, 184 insertions(+), 127 deletions(-) diff --git a/.gitignore b/.gitignore index 5f57fe4..4a7d880 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ +*.log home.conf ovh.conf -zone.list -*.log +zone.json diff --git a/dyndomain b/dyndomain index 4f3e0a0..463527e 100755 --- a/dyndomain +++ b/dyndomain @@ -1,10 +1,12 @@ #!/usr/bin/python3 -import sys -import os import configparser -import subprocess +import copy +import json +import os import smtplib +import subprocess +import sys import time from email.message import EmailMessage from pprint import pprint @@ -19,8 +21,8 @@ from ovh import ovh def load_conf(): global zone_filename, log_filename - global wan_hostname, zone_domain, zone_subdomain - global hosts_list, hosts_ipv4_nat_list, hosts_alias_list + global wan_hostname, zone_domain, zone_subdomain, zone_timeout + global hosts_list, nat_list, pinhole_list global mail_from, mail_to, mail_ignore_list conf = configparser.ConfigParser(allow_no_value=True) @@ -38,8 +40,31 @@ def load_conf(): mail_ignore_list = [ '.'.join([h, zone_subdomain]) if not h.endswith('.'+zone_subdomain) else h for h in mail_ignore_list ] hosts_list = [ host.lower() for host in conf['Hosts'] ] - hosts_ipv4_nat_list = [ host.lower() for host in conf['NatHosts'] ] - hosts_alias_list = conf['Aliases'] + + nat_list = {} + for e in conf.items('PortsNat'): + if e[1]: + nat_list[e[0]] = e[1].split(',') + else: + nat_list[e[0]] = [] + + pinhole_list = {} + for e in conf.items('Firewall'): + if e[1]: + pinhole_list[e[0]] = e[1].split(',') + else: + pinhole_list[e[0]] = [] + zone_timeout = conf['Zone']['keep'] + if zone_timeout[-1] == 's': + zone_timeout = int(zone_timeout[:-1]) + elif zone_timeout[-1] == 'm': + zone_timeout = int(zone_timeout[:-1]) * 60 + elif zone_timeout[-1] == 'h': + zone_timeout = int(zone_timeout[:-1]) * 3600 + elif zone_timeout[-1] == 'd': + zone_timeout = int(zone_timeout[:-1]) * 86400 + else: + zone_timeout = int(zone_timeout) def ping(hostname): @@ -55,9 +80,14 @@ def get_ipv6_hosts(): for h in r['status']: if not 'IPv6Address' in h or not h['IPv6Address']: continue - hostname = h['Name'].lower() - if hostname in hosts_alias_list: - hostname = hosts_alias_list[hostname] + ns = {} + for n in h['Names']: + ns[n['Source']] = n['Name'] + hostname = h['Name'] + hostname = ns.get('mdns', hostname) + hostname = ns.get('dhcp', hostname) + hostname = ns.get('webui', hostname) + hostname = hostname.lower() for a in h['IPv6Address']: if a['Scope'] != 'global' or a['Status'] != 'reachable': continue @@ -82,60 +112,101 @@ def full_name(host, domain): return '.'.join([host, domain]) -def make_zone_list(wan_hostname, wan_addr, hosts, hosts_nat, domain): - zone = [] +def zone_add_entry(zone, prot, name, addr, stamp): + #print('zone_add_entry: {} {} {} {}'.format(prot, name, addr, stamp)) + if not zone.get(prot): + zone[prot] = {} + if not zone[prot].get(name): + zone[prot][name] = {} + if not zone[prot][name].get(addr): + zone[prot][name][addr] = { 'first': stamp } + zone[prot][name][addr]['last'] = stamp + + +def populate_zone(zone, wan_hostname, wan_addr, hosts, hosts_list, hosts_nat, domain, stamp): wan_hostname = full_name(wan_hostname, domain) + if not zone.get('A'): + zone['A'] = {} + if not zone.get('AAAA'): + zone['AAAA'] = {} + if wan_addr['ipv4'] != '': - zone.append([wan_hostname, 'A', wan_addr['ipv4']]) + zone_add_entry(zone, 'A', wan_hostname, wan_addr['ipv4'], stamp) for host in hosts_nat: - zone.append([full_name(host, domain), 'A', wan_addr['ipv4']]) + if hosts.get(host): + zone_add_entry(zone, 'A', full_name(host, domain), wan_addr['ipv4'], stamp) if wan_addr['ipv6'] != '': - zone.append([wan_hostname, 'AAAA', wan_addr['ipv6']]) - for host in hosts: - for addr in hosts[host]: - if host in hosts_list: - zone.append([full_name(host, domain), 'AAAA', addr]) + zone_add_entry(zone, 'AAAA', wan_hostname, wan_addr['ipv6'], stamp) + for host in hosts_list: + for addr in hosts.get(host, []): + zone_add_entry(zone, 'AAAA', full_name(host, domain), addr, stamp) - return zone +def process_zone(zone, stamp, grace_period): + update = { 'add': [], 'delete': [] } + for prot in zone: + for name in zone[prot]: + add = False + for addr in zone[prot][name]: + e = zone[prot][name][addr] + if e['first'] == stamp: + update['add'].append([prot, name, addr]) + add = True + for addr in zone[prot][name]: + e = zone[prot][name][addr] + if add and e['last'] < stamp: + update['delete'].append([prot, name, addr]) + elif not add and stamp - e['last'] > grace_period: + update['delete'].append([prot, name, addr]) + + if not update['add'] and not update['delete']: + update = None + return update -def read_zone_list_from_file(zone_filename): - zone_list = [] - config = configparser.ConfigParser() - config.read(zone_filename) - for host in config: - for p in config[host]: - for target in config[host][p].split('\n'): - zone_list.append([host.lower(), p.upper(), target]) - return zone_list +def read_zone_list(zone_filename): + try: + with open(zone_filename) as jsonfile: + zone = json.load(jsonfile) + return zone + except FileNotFoundError: + return {} -def make_delete_zone_list(zone_list, prev_zone_list): - return make_update_zone_list(prev_zone_list, zone_list) +def write_zone_list(zone_filename, zone, update): + zone = copy.deepcopy(zone) + if update: + for prot, name, addr in update['delete']: + del zone[prot][name][addr] + if not zone[prot][name]: + del zone[prot][name] + with open(zone_filename, 'w') as jsonfile: + json.dump(zone, jsonfile, indent=2, sort_keys=True) -def make_update_zone_list(zone_list, prev_zone_list): - update_zone_list = [] - for entry in zone_list: - if not any(entry == x for x in prev_zone_list): - update_zone_list.append(entry) - return update_zone_list - - -def write_zone_list_to_file(zone_filename, zone_list): - config = configparser.ConfigParser() - for host,typefield,target in zone_list: - if not host in config: - config[host] = {} - if typefield in config[host]: - config[host][typefield] += '\n'+target - else: - config[host][typefield] = target - with open(zone_filename, 'w') as configfile: - config.write(configfile) +#def make_delete_zone_list(zone_list, prev_zone_list): +# return make_update_zone_list(prev_zone_list, zone_list) +# +# +#def make_update_zone_list(zone_list, prev_zone_list): +# update_zone_list = [] +# for entry in zone_list: +# if not any(entry == x for x in prev_zone_list): +# update_zone_list.append(entry) +# return update_zone_list +# +# +# for host,typefield,target in zone_list: +# if not host in config: +# config[host] = {} +# if typefield in config[host]: +# config[host][typefield] += '\n'+target +# else: +# config[host][typefield] = target +# with open(zone_filename, 'w') as configfile: +# config.write(configfile) def log(msg): @@ -145,49 +216,38 @@ def log(msg): logfile.write("%s - %s\n" % (stamp, msg)) -def log_update_zone(zone_list, delete_zone_list): - for host,typea,addr in delete_zone_list: - log("[DEL] %-20s %-6s %s" % (host, typea, addr)) - for host,typea,addr in zone_list: - log(" %-20s %-6s %s" % (host, typea, addr)) +def log_update_zone(update): + op_str = { 'add': '', 'delete': '[DEL]' } + + for op in update: + for prot, name, addr in update[op]: + op = op_str.get(op, op) + log('%5s %-20s %-6s %s' % (op, name, prot, addr)) -def ovh_update_zone(domain, update_zone_list, delete_zone_list): - if not len(update_zone_list) and not len(delete_zone_list): - return False - +def ovh_update_zone(domain, update): try: client = ovh.Client() - for host, fieldtype, target in delete_zone_list: + for prot, name, addr in update['delete']: result = client.get('/domain/zone/%s/record' % domain, - fieldType=fieldtype, - subDomain=host) + fieldType=prot, + subDomain=name) for id in result: r = client.get('/domain/zone/%s/record/%d' % (domain, id)) - if r['fieldType'] == fieldtype and r['target'] == target: - #print("Delete entry for %s %s %s" % (host, fieldtype, target)) + if r['fieldType'] == prot and r['target'] == addr: + print("OVH: delete entry for %s %s %s (#%s)" % (name, prot, addr, id)) client.delete('/domain/zone/%s/record/%d' % (domain, id)) - for host, fieldtype, target in update_zone_list: - result = client.get('/domain/zone/%s/record' % domain, - fieldType=fieldtype, - subDomain=host) - skip = False - for id in result: - r = client.get('/domain/zone/%s/record/%d' % (domain, id)) - if r['fieldType'] == fieldtype and r['target'] == target: - skip = True - if skip: - continue - #print("Create new entry for %s %s %s" % (host, fieldtype, target)) + for prot, name, addr in update['add']: + print("OVH: create entry for %s %s %s" % (name, prot, addr)) client.post('/domain/zone/%s/record' % domain, - fieldType=fieldtype, - subDomain=host, - target=target, + fieldType=prot, + subDomain=name, + target=addr, ttl=60) - #print("Refresh zone %s" % domain) + print("OVH: Refresh zone %s" % domain) client.post('/domain/zone/%s/refresh' % domain) return True except: @@ -195,8 +255,18 @@ def ovh_update_zone(domain, update_zone_list, delete_zone_list): return False -def send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, delete_zone_list, mail_ignore_list, wan): - #print('Send email to %s' % mail_to) +def send_update_mail(mail_to, mail_from, zone_domain, update, mail_ignore_list, wan): + okmail=False + for op in update: + for prot,name,addr in update[op]: + if not name in mail_ignore_list: + okmail = True + break + + if not okmail: + return + + print('Send email to %s' % mail_to) msg = EmailMessage() msg['Subject'] = "Livebox update in %s" % zone_domain @@ -209,25 +279,22 @@ def send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, delete_z txt = txt + "\nZone %s has been updated:\n" % zone_domain - filtered_update_list = [ h for h in update_zone_list if not h[0] in mail_ignore_list ] - filtered_delete_list = [ h for h in delete_zone_list if not h[0] in mail_ignore_list ] + for prot,name,addr in update['add']: + txt = txt + " %-20s %-4s %s\n" % (name,prot,addr) - if len(filtered_update_list) or len(filtered_delete_list): - for host,tp,addr in update_zone_list: - txt = txt + " %-20s %-4s %s\n" % (host,tp,addr) + txt = txt + "\nRemoved entries:\n" - txt = txt + "\nRemoved entries:\n" + for prot,name,addr in update['delete']: + txt = txt + " %-20s %-4s %s\n" % (name,prot,addr) - for host,tp,addr in delete_zone_list: - txt = txt + " %-20s %-4s %s\n" % (host,tp,addr) + txt = txt + '\n' - txt = txt + '\n' + msg.set_content(txt) - msg.set_content(txt) - - s = smtplib.SMTP('localhost') - s.send_message(msg) - s.quit() + s = smtplib.SMTP('localhost') + s.send_message(msg) + s.quit() + #print(txt) load_conf() @@ -245,27 +312,15 @@ if not r: hosts = get_ipv6_hosts() wan = get_wan_addr() -zone_list = make_zone_list(wan_hostname, wan, hosts, hosts_ipv4_nat_list, zone_subdomain) -prev_zone_list = read_zone_list_from_file(zone_filename) -delete_zone_list = make_delete_zone_list(zone_list, prev_zone_list) -update_zone_list = make_update_zone_list(zone_list, prev_zone_list) - -#if update_zone_list or delete_zone_list: -# print('zone_list:') -# pprint(zone_list) -# print('prev_zone_list:') -# pprint(prev_zone_list) -# print('update_zone_list:') -# pprint(update_zone_list) -# print('delete_zone_list:') -# pprint(delete_zone_list) - -log_update_zone(update_zone_list, delete_zone_list) - -success = ovh_update_zone(zone_domain, update_zone_list, delete_zone_list) - -if success: - new_zone_list = write_zone_list_to_file(zone_filename, zone_list) - send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, delete_zone_list, mail_ignore_list, wan) +zone = read_zone_list(zone_filename) +stamp = int(time.time()) +populate_zone(zone, wan_hostname, wan, hosts, hosts_list, nat_list, zone_subdomain, stamp) +update = process_zone(zone, stamp, zone_timeout) +if update: + log_update_zone(update) + success = ovh_update_zone(zone_domain, update) + if success: + send_update_mail(mail_to, mail_from, zone_domain, update, mail_ignore_list, wan) +write_zone_list(zone_filename, zone, update) diff --git a/home-sample.conf b/home-sample.conf index 8f8e676..779d0ff 100644 --- a/home-sample.conf +++ b/home-sample.conf @@ -2,7 +2,7 @@ # edit it and save it as home.conf [Files] -zonefile = zone.list +zonefile = zone.json logfile = zone.log [Wan] @@ -13,20 +13,22 @@ mypc1 mypc2 mypc3 -[Aliases] -mypc1-wifi = mypc1 +[PortsNat] +mypc2 = 22,443 +mypc3 = 10022 -[NatHosts] -mypc2 -mypc4 +[Firewall] +mypc2 = 22,443 +mypc3 = 10022 [Zone] domain = example.com subdomain = home +keep = 3d [Mail] from = me@example.com to = you@example.com [MailIgnore] -mypc2.home +mypc2