big update

* zone.list moved to zone.json
* entries are deleted from zone only after a grace period
* preparation for firewall support
This commit is contained in:
Gilles Grandou 2020-10-03 23:18:00 +02:00
parent f4652bfc12
commit 6cb57c6dbf
3 changed files with 184 additions and 127 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
*.log
home.conf
ovh.conf
zone.list
*.log
zone.json

291
dyndomain
View File

@ -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)

View File

@ -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