2018-02-19 23:42:57 +01:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
import configparser
|
2020-10-03 23:18:00 +02:00
|
|
|
import copy
|
|
|
|
import json
|
|
|
|
import os
|
2018-02-19 23:42:57 +01:00
|
|
|
import smtplib
|
2020-10-03 23:18:00 +02:00
|
|
|
import subprocess
|
|
|
|
import sys
|
2018-02-19 23:42:57 +01:00
|
|
|
import time
|
|
|
|
from email.message import EmailMessage
|
|
|
|
from pprint import pprint
|
|
|
|
|
|
|
|
rundir=os.path.realpath(os.path.dirname(sys.argv[0]))
|
|
|
|
os.chdir(rundir)
|
|
|
|
|
2020-02-05 17:52:43 +01:00
|
|
|
sys.path.append(os.path.join(rundir,'sysbus/src'))
|
2018-02-19 23:42:57 +01:00
|
|
|
from sysbus import sysbus
|
|
|
|
from ovh import ovh
|
|
|
|
|
|
|
|
|
|
|
|
def load_conf():
|
|
|
|
global zone_filename, log_filename
|
2020-10-03 23:18:00 +02:00
|
|
|
global wan_hostname, zone_domain, zone_subdomain, zone_timeout
|
|
|
|
global hosts_list, nat_list, pinhole_list
|
2019-10-10 10:52:12 +02:00
|
|
|
global mail_from, mail_to, mail_ignore_list
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
conf = configparser.ConfigParser(allow_no_value=True)
|
|
|
|
conf.read('home.conf')
|
|
|
|
|
|
|
|
zone_filename = conf['Files']['zonefile']
|
|
|
|
log_filename = conf['Files']['logfile']
|
|
|
|
|
2019-12-16 18:27:32 +01:00
|
|
|
wan_hostname = conf['Wan']['hostname'].lower()
|
|
|
|
zone_domain = conf['Zone']['domain'].lower()
|
|
|
|
zone_subdomain = conf['Zone']['subdomain'].lower()
|
2018-02-19 23:42:57 +01:00
|
|
|
mail_from = conf['Mail']['from']
|
|
|
|
mail_to = conf['Mail']['to']
|
2019-12-16 18:40:41 +01:00
|
|
|
mail_ignore_list = [ host.lower() for host in conf['MailIgnore'] ]
|
|
|
|
mail_ignore_list = [ '.'.join([h, zone_subdomain]) if not h.endswith('.'+zone_subdomain) else h for h in mail_ignore_list ]
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2019-12-16 18:27:32 +01:00
|
|
|
hosts_list = [ host.lower() for host in conf['Hosts'] ]
|
2020-10-03 23:18:00 +02:00
|
|
|
|
|
|
|
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)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
def ping(hostname):
|
|
|
|
cmd = "ping -c1 -w3 %s" % hostname
|
|
|
|
ret = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
return 0 if ret.returncode else 1
|
|
|
|
|
2019-10-07 09:19:16 +02:00
|
|
|
|
2020-10-04 15:45:15 +02:00
|
|
|
def get_hosts():
|
2019-10-05 23:01:08 +02:00
|
|
|
r = sysbus.requete('Devices:get')
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-04 15:45:15 +02:00
|
|
|
hosts = { 'A': {}, 'AAAA': {} }
|
2019-10-05 23:01:08 +02:00
|
|
|
for h in r['status']:
|
2020-10-03 23:18:00 +02:00
|
|
|
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()
|
2020-10-04 15:45:15 +02:00
|
|
|
for a in h.get('IPv6Address', []):
|
2019-10-05 23:01:08 +02:00
|
|
|
if a['Scope'] != 'global' or a['Status'] != 'reachable':
|
|
|
|
continue
|
2020-10-04 15:45:15 +02:00
|
|
|
if not hostname in hosts['AAAA']:
|
|
|
|
hosts['AAAA'][hostname] = []
|
|
|
|
hosts['AAAA'][hostname].append(a['Address'])
|
|
|
|
for a in h.get('IPv4Address', []):
|
|
|
|
if a['Scope'] != 'global' or a['Status'] != 'reachable':
|
|
|
|
continue
|
|
|
|
if not hostname in hosts['A']:
|
|
|
|
hosts['A'][hostname] = []
|
|
|
|
hosts['A'][hostname].append(a['Address'])
|
|
|
|
return hosts
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
def get_wan_addr():
|
|
|
|
r = sysbus.requete('NMC:getWANStatus')
|
|
|
|
wan = dict()
|
|
|
|
wan['ipv4'] = r['data']['IPAddress']
|
|
|
|
wan['ipv6'] = r['data']['IPv6Address']
|
2019-12-17 13:58:54 +01:00
|
|
|
if wan['ipv4'] == '0.0.0.0':
|
|
|
|
wan['ipv4'] = ''
|
2018-02-19 23:42:57 +01:00
|
|
|
return wan
|
|
|
|
|
|
|
|
|
|
|
|
def full_name(host, domain):
|
|
|
|
host = host.replace('_', '-')
|
|
|
|
return '.'.join([host, domain])
|
|
|
|
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
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):
|
2018-02-19 23:42:57 +01:00
|
|
|
wan_hostname = full_name(wan_hostname, domain)
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
if not zone.get('A'):
|
|
|
|
zone['A'] = {}
|
|
|
|
if not zone.get('AAAA'):
|
|
|
|
zone['AAAA'] = {}
|
|
|
|
|
2018-02-19 23:42:57 +01:00
|
|
|
if wan_addr['ipv4'] != '':
|
2020-10-03 23:18:00 +02:00
|
|
|
zone_add_entry(zone, 'A', wan_hostname, wan_addr['ipv4'], stamp)
|
2018-02-19 23:42:57 +01:00
|
|
|
for host in hosts_nat:
|
2020-10-04 15:45:15 +02:00
|
|
|
if hosts['A'].get(host):
|
2020-10-03 23:18:00 +02:00
|
|
|
zone_add_entry(zone, 'A', full_name(host, domain), wan_addr['ipv4'], stamp)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
if wan_addr['ipv6'] != '':
|
2020-10-03 23:18:00 +02:00
|
|
|
zone_add_entry(zone, 'AAAA', wan_hostname, wan_addr['ipv6'], stamp)
|
|
|
|
for host in hosts_list:
|
2020-10-04 15:45:15 +02:00
|
|
|
for addr in hosts['AAAA'].get(host, []):
|
2020-10-03 23:18:00 +02:00
|
|
|
zone_add_entry(zone, 'AAAA', full_name(host, domain), addr, stamp)
|
|
|
|
|
2020-10-04 15:25:16 +02:00
|
|
|
|
|
|
|
def process_zone(zone, stamp, grace_period, sync_zone):
|
2020-10-03 23:18:00 +02:00
|
|
|
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]
|
2020-10-04 15:25:16 +02:00
|
|
|
if e['first'] == stamp and not sync_zone:
|
2020-10-03 23:18:00 +02:00
|
|
|
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])
|
2020-10-04 15:25:16 +02:00
|
|
|
elif sync_zone:
|
|
|
|
update['add'].append([prot, name, addr])
|
2020-10-03 23:18:00 +02:00
|
|
|
|
|
|
|
if not update['add'] and not update['delete']:
|
|
|
|
update = None
|
|
|
|
return update
|
|
|
|
|
|
|
|
|
|
|
|
def read_zone_list(zone_filename):
|
|
|
|
try:
|
|
|
|
with open(zone_filename) as jsonfile:
|
|
|
|
zone = json.load(jsonfile)
|
|
|
|
return zone
|
|
|
|
except FileNotFoundError:
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2018-02-19 23:42:57 +01:00
|
|
|
def log(msg):
|
|
|
|
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
with open(log_filename, 'a') as logfile:
|
|
|
|
for line in msg.split('\n'):
|
|
|
|
logfile.write("%s - %s\n" % (stamp, msg))
|
|
|
|
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
def log_update_zone(update):
|
|
|
|
op_str = { 'add': '', 'delete': '[DEL]' }
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
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))
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
|
2020-10-04 15:25:16 +02:00
|
|
|
def ovh_update_zone(domain, zone, update, sync_zone):
|
2019-11-12 12:23:26 +01:00
|
|
|
try:
|
|
|
|
client = ovh.Client()
|
|
|
|
|
2020-10-04 15:25:16 +02:00
|
|
|
if sync_zone:
|
|
|
|
for prot in zone:
|
|
|
|
for name in zone[prot]:
|
|
|
|
|
|
|
|
result = client.get('/domain/zone/{}/record'.format(domain),
|
|
|
|
fieldType=prot, subDomain=name)
|
|
|
|
for id in result:
|
|
|
|
print('OVH: delete entry #{} {} {}'.format(id, name, prot))
|
|
|
|
client.delete('/domain/zone/{}/record/{}'.format(domain, id))
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
for prot, name, addr in update['delete']:
|
2019-11-12 12:23:26 +01:00
|
|
|
result = client.get('/domain/zone/%s/record' % domain,
|
2020-10-03 23:18:00 +02:00
|
|
|
fieldType=prot,
|
|
|
|
subDomain=name)
|
2019-11-12 12:23:26 +01:00
|
|
|
for id in result:
|
|
|
|
r = client.get('/domain/zone/%s/record/%d' % (domain, id))
|
2020-10-03 23:18:00 +02:00
|
|
|
if r['fieldType'] == prot and r['target'] == addr:
|
|
|
|
print("OVH: delete entry for %s %s %s (#%s)" % (name, prot, addr, id))
|
2019-11-12 12:23:26 +01:00
|
|
|
client.delete('/domain/zone/%s/record/%d' % (domain, id))
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
for prot, name, addr in update['add']:
|
|
|
|
print("OVH: create entry for %s %s %s" % (name, prot, addr))
|
2019-11-12 12:23:26 +01:00
|
|
|
client.post('/domain/zone/%s/record' % domain,
|
2020-10-03 23:18:00 +02:00
|
|
|
fieldType=prot,
|
|
|
|
subDomain=name,
|
|
|
|
target=addr,
|
2019-11-12 12:23:26 +01:00
|
|
|
ttl=60)
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
print("OVH: Refresh zone %s" % domain)
|
2019-11-12 12:23:26 +01:00
|
|
|
client.post('/domain/zone/%s/refresh' % domain)
|
|
|
|
return True
|
|
|
|
except:
|
|
|
|
print('OVH update error\n')
|
|
|
|
return False
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
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)
|
2018-02-19 23:42:57 +01:00
|
|
|
msg = EmailMessage()
|
2019-10-05 23:03:38 +02:00
|
|
|
|
2018-02-19 23:42:57 +01:00
|
|
|
msg['Subject'] = "Livebox update in %s" % zone_domain
|
|
|
|
msg['From'] = mail_from
|
|
|
|
msg['To' ] = mail_to
|
|
|
|
|
|
|
|
txt = "Livebox update\n\n"
|
|
|
|
txt = txt + "WAN IPv4 : %s\n" % wan['ipv4']
|
|
|
|
txt = txt + "WAN IPv6 : %s\n" % wan['ipv6']
|
|
|
|
|
|
|
|
txt = txt + "\nZone %s has been updated:\n" % zone_domain
|
2019-10-05 23:03:38 +02:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
for prot,name,addr in update['add']:
|
|
|
|
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
|
2019-10-05 23:03:38 +02:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
txt = txt + "\nRemoved entries:\n"
|
2019-10-05 23:03:38 +02:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
for prot,name,addr in update['delete']:
|
|
|
|
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
txt = txt + '\n'
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
msg.set_content(txt)
|
2019-10-10 10:52:12 +02:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
s = smtplib.SMTP('localhost')
|
|
|
|
s.send_message(msg)
|
|
|
|
s.quit()
|
|
|
|
#print(txt)
|
2019-10-05 23:03:38 +02:00
|
|
|
|
2018-02-19 23:42:57 +01:00
|
|
|
|
|
|
|
load_conf()
|
|
|
|
|
|
|
|
if not ping(wan_hostname):
|
|
|
|
log("%s is down" % wan_hostname)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
|
|
sysbus.load_conf()
|
2019-10-05 23:03:38 +02:00
|
|
|
r = sysbus.auth(False)
|
|
|
|
if not r:
|
|
|
|
print('Error: cannot authenticate on livebox')
|
|
|
|
sys.exit(1)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-04 15:45:15 +02:00
|
|
|
hosts = get_hosts()
|
2018-02-19 23:42:57 +01:00
|
|
|
wan = get_wan_addr()
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
zone = read_zone_list(zone_filename)
|
2020-10-04 15:25:16 +02:00
|
|
|
sync_zone = False
|
|
|
|
if not zone:
|
|
|
|
sync_zone = True
|
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
stamp = int(time.time())
|
|
|
|
populate_zone(zone, wan_hostname, wan, hosts, hosts_list, nat_list, zone_subdomain, stamp)
|
2020-10-04 15:25:16 +02:00
|
|
|
update = process_zone(zone, stamp, zone_timeout, sync_zone)
|
2020-10-03 23:18:00 +02:00
|
|
|
if update:
|
|
|
|
log_update_zone(update)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-04 15:25:16 +02:00
|
|
|
success = ovh_update_zone(zone_domain, zone, update, sync_zone)
|
2020-10-03 23:18:00 +02:00
|
|
|
if success:
|
|
|
|
send_update_mail(mail_to, mail_from, zone_domain, update, mail_ignore_list, wan)
|
2018-02-19 23:42:57 +01:00
|
|
|
|
2020-10-03 23:18:00 +02:00
|
|
|
write_zone_list(zone_filename, zone, update)
|