27 Commits

Author SHA1 Message Date
2f970cd71c add livebox firewall support 2020-10-04 17:41:05 +02:00
677db2e9a6 get hosts list with both IPv6 and IPv4 addresses 2020-10-04 15:46:01 +02:00
893327c31d sync ovh zone is zone.json does not exists 2020-10-04 15:25:16 +02:00
a30efabbda clean 2020-10-04 15:02:56 +02:00
6cb57c6dbf big update
* zone.list moved to zone.json
* entries are deleted from zone only after a grace period
* preparation for firewall support
2020-10-04 11:16:13 +02:00
f4652bfc12 ovh update 2020-10-04 11:11:31 +02:00
2534a41a27 sysbus update 2020-10-04 11:11:26 +02:00
819df7f6e1 support new sysbus layout 2020-02-05 17:52:43 +01:00
8c4f998018 bug fix host alias list 2020-01-03 15:53:16 +01:00
055f4949cd filter out invalid 0.0.0.0 ipv4 2019-12-17 13:58:54 +01:00
79337e8411 document MailIgnore 2019-12-16 18:51:15 +01:00
11c20250b9 MailIgnore section can contains simple hostnames
it's no longer needed to put host.subdomain
2019-12-16 18:40:41 +01:00
0616e22e0c convert hostnames to lowercase 2019-12-16 18:31:33 +01:00
79eb79f46c catch ovh_update errors 2019-11-12 12:23:26 +01:00
5aa8575127 ovh update fixes 2019-11-12 12:20:50 +01:00
6894ee25d9 typo fix 2019-11-12 12:19:51 +01:00
7fb17678b8 debug update 2019-11-12 12:19:27 +01:00
2609ce5f6d Add MailIgnore option support 2019-10-10 22:49:41 +02:00
7599ec2684 fixed multiple entries ovh update 2019-10-10 10:55:00 +02:00
bdcf586a6c add support for multiple addresses in zone.list 2019-10-10 10:55:00 +02:00
95a20f78e5 git ignore log files 2019-10-10 10:55:00 +02:00
67335c198b register_ovh_account: pass domain as argument 2019-10-10 10:55:00 +02:00
306d4b536e cosmetic log update 2019-10-10 10:55:00 +02:00
df66e50d47 add [Aliases] support 2019-10-10 10:55:00 +02:00
841e6043c6 only update ovh zone with diffs 2019-10-05 23:03:38 +02:00
2cc7d966a0 fix ipv6 sysbus request 2019-10-05 23:01:08 +02:00
6c4bc82319 sysbus: update 2019-10-05 19:33:59 +02:00
7 changed files with 306 additions and 115 deletions

4
.gitignore vendored
View File

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

4
README
View File

@ -143,6 +143,10 @@ Setup - Réseau local
Chaque mise à jour génerera un mail qui sera envoyé à cette adresse.
* configuration [MailIgnore]:
Si les mises à jour ne concernent que des entrées dans cette liste
le mail ne sera pas envoyé
Utilisation
-----------

387
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
@ -12,15 +14,16 @@ from pprint import pprint
rundir=os.path.realpath(os.path.dirname(sys.argv[0]))
os.chdir(rundir)
sys.path.append(os.path.join(rundir,'sysbus/src'))
from sysbus import sysbus
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
global mail_from, mail_to
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)
conf.read('home.conf')
@ -28,14 +31,40 @@ def load_conf():
zone_filename = conf['Files']['zonefile']
log_filename = conf['Files']['logfile']
wan_hostname = conf['Wan']['hostname']
zone_domain = conf['Zone']['domain']
zone_subdomain = conf['Zone']['subdomain']
wan_hostname = conf['Wan']['hostname'].lower()
zone_domain = conf['Zone']['domain'].lower()
zone_subdomain = conf['Zone']['subdomain'].lower()
mail_from = conf['Mail']['from']
mail_to = conf['Mail']['to']
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 ]
hosts_list = [ host for host in conf['Hosts'] ]
hosts_ipv4_nat_list = [ host for host in conf['NatHosts'] ]
hosts_list = [ host.lower() for host in conf['Hosts'] ]
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):
@ -43,17 +72,33 @@ def ping(hostname):
ret = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return 0 if ret.returncode else 1
def get_ipv6_hosts():
r = sysbus.requete('Hosts:getDevices')
ipv6_hosts = dict()
for host in r['status']:
hostname=host['hostName']
for addr in host['addresses']:
ipv6_hosts[hostname] = []
if addr['family'] == 'IPv6' and addr['scope'] == 'global':
ipv6_hosts[hostname].append(addr['ipAddress'])
return ipv6_hosts
def get_hosts():
r = sysbus.requete('Devices:get')
hosts = { 'A': {}, 'AAAA': {} }
for h in r['status']:
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.get('IPv6Address', []):
if a['Scope'] != 'global' or a['Status'] != 'reachable':
continue
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
def get_wan_addr():
@ -61,6 +106,8 @@ def get_wan_addr():
wan = dict()
wan['ipv4'] = r['data']['IPAddress']
wan['ipv6'] = r['data']['IPv6Address']
if wan['ipv4'] == '0.0.0.0':
wan['ipv4'] = ''
return wan
@ -69,52 +116,87 @@ 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, key, value, stamp):
#print('zone_add_entry: {} {} {} {}'.format(prot, key, value, stamp))
if not zone.get(prot):
zone[prot] = {}
if not zone[prot].get(key):
zone[prot][key] = {}
if not zone[prot][key].get(value):
zone[prot][key][value] = { 'first': stamp }
zone[prot][key][value]['last'] = stamp
def populate_zone(zone, wan_hostname, wan_addr, hosts, hosts_list, hosts_nat, pinhole_list, 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['A'].get(host):
zone_add_entry(zone, 'A', full_name(host, domain), wan_addr['ipv4'], stamp)
# we can only add PortNat entry for on Address,
# so let's arbitrarely take the 1st one
for port in hosts_nat[host]:
zone_add_entry(zone, 'nat', hosts['A'][host][0], port, 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])
return zone
zone_add_entry(zone, 'AAAA', wan_hostname, wan_addr['ipv6'], stamp)
for host in hosts_list:
for addr in hosts['AAAA'].get(host, []):
zone_add_entry(zone, 'AAAA', full_name(host, domain), addr, stamp)
for port in pinhole_list.get(host, []):
zone_add_entry(zone, 'pin', addr, port, stamp)
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]:
zone_list.append([host, p.upper(), config[host][p]])
return zone_list
def process_zone(zone, stamp, grace_period, sync_zone):
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 and not sync_zone:
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])
elif sync_zone:
update['add'].append([prot, name, addr])
if not update['add'] and not update['delete']:
update = None
return update
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 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_to_file(zone_filename, zone_list):
config = configparser.ConfigParser()
for host,typefield,target in zone_list:
if not host in config:
config[host] = {}
config[host][typefield] = target
with open(zone_filename, 'w') as configfile:
config.write(configfile)
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 log(msg):
@ -124,43 +206,129 @@ def log(msg):
logfile.write("%s - %s\n" % (stamp, msg))
def log_update_zone(zone_list):
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, zone_list):
if not len(zone_list):
def ovh_update_zone(domain, zone, update, sync_zone):
try:
client = ovh.Client()
if sync_zone:
for prot in ['A', 'AAAA']:
for name in zone.get(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))
for prot, name, addr in update['delete']:
if not prot in ['A', 'AAAA']:
continue
result = client.get('/domain/zone/%s/record' % domain,
fieldType=prot,
subDomain=name)
for id in result:
r = client.get('/domain/zone/%s/record/%d' % (domain, id))
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 prot, name, addr in update['add']:
if not prot in ['A', 'AAAA']:
continue
print("OVH: create entry for %s %s %s" % (name, prot, addr))
client.post('/domain/zone/%s/record' % domain,
fieldType=prot,
subDomain=name,
target=addr,
ttl=60)
print("OVH: Refresh zone %s" % domain)
client.post('/domain/zone/%s/refresh' % domain)
return True
except:
print('OVH update error\n')
return False
client = ovh.Client()
def livebox_rule_id(words):
id='dyndomain_{}'.format('_'.join(words))
id = id.replace('.', '_')
return id
def livebox_delete_port_nat(port, addr):
print('livebox: delete PortNat {} to {}'.format(port, addr))
id = livebox_rule_id([port])
r = sysbus.requete('Firewall:deletePortForwarding', { 'id': id, 'origin': 'webui' })
for host, fieldtype, target in zone_list:
result = client.get('/domain/zone/%s/record' % domain,
fieldType=fieldtype,
subDomain=host)
if len(result) == 0:
#print("Create new entry for %s %s %s" % (host, fieldtype, target))
client.post('/domain/zone/%s/record' % domain,
fieldType=fieldtype,
subDomain=host,
target=target,
ttl=60)
else:
id = result[0]
#print("Update entry for %s %s %s" % (host, fieldtype, target))
client.put('/domain/zone/%s/record/%ld' % (domain, id),
target=target)
def livebox_delete_pinhole(port, addr):
print('livebox: delete pinhole {} to {}'.format(port, addr))
id = livebox_rule_id([port, addr])
r = sysbus.requete('Firewall:deletePinhole', { 'id': id, 'origin': 'webui' })
#print("Refresh zone %s" % domain)
client.post('/domain/zone/%s/refresh' % domain)
return True
def livebox_add_port_nat(port, addr):
print('livebox: add PortNat {} to {}'.format(port, addr))
id = livebox_rule_id([port])
a = {
'id': id,
'origin': 'webui',
'sourceInterface': 'data',
'destinationIPAddress': addr,
'protocol': '6',
'internalPort': port,
'enable': True,
}
r = sysbus.requete('Firewall:setPortForwarding', a)
def livebox_add_pinhole(port, addr):
print('livebox: add pinhole {} to {}'.format(port, addr))
id = livebox_rule_id([port, addr])
a = {
'id': id,
'origin': 'webui',
'sourceInterface': 'data',
'destinationPort': port,
'destinationIPAddress': addr,
'protocol': '6',
'enable': True,
}
r = sysbus.requete('Firewall:setPinhole', a)
def livebox_update_fw(zone, update, sync_zone):
for prot, addr, port in update['delete']:
if prot == 'nat':
livebox_delete_port_nat(port, addr)
elif prot == 'pin':
livebox_delete_pinhole(port, addr)
for prot, addr, port in update['add']:
if prot == 'nat':
livebox_add_port_nat(port, addr)
elif prot == 'pin':
livebox_add_pinhole(port, addr)
def send_update_mail(mail_to, mail_from, zone_domain, update_zone_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
msg['From'] = mail_from
msg['To' ] = mail_to
@ -170,9 +338,15 @@ def send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, wan):
txt = txt + "WAN IPv6 : %s\n" % wan['ipv6']
txt = txt + "\nZone %s has been updated:\n" % zone_domain
for host,tp,addr in update_zone_list:
txt = txt + " %-20s %-4s %s\n" % (host,tp,addr)
for prot,name,addr in update['add']:
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
txt = txt + "\nRemoved entries:\n"
for prot,name,addr in update['delete']:
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
txt = txt + '\n'
msg.set_content(txt)
@ -180,7 +354,8 @@ def send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, wan):
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
#print(txt)
load_conf()
@ -189,28 +364,28 @@ if not ping(wan_hostname):
sys.exit(0)
sysbus.load_conf()
sysbus.auth()
r = sysbus.auth(False)
if not r:
print('Error: cannot authenticate on livebox')
sys.exit(1)
hosts = get_ipv6_hosts()
hosts = get_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)
update_zone_list = make_update_zone_list(zone_list, prev_zone_list)
zone = read_zone_list(zone_filename)
sync_zone = False
if not zone:
sync_zone = True
#print('zone_list:')
#pprint(zone_list)
#print('prev_zone_list:')
#pprint(prev_zone_list)
#print('update_zone_list:')
#pprint(update_zone_list)
log_update_zone(update_zone_list)
sucess = ovh_update_zone(zone_domain, update_zone_list)
if sucess:
new_zone_list = write_zone_list_to_file(zone_filename, prev_zone_list+update_zone_list)
send_update_mail(mail_to, mail_from, zone_domain, update_zone_list, wan)
stamp = int(time.time())
populate_zone(zone, wan_hostname, wan, hosts, hosts_list, nat_list, pinhole_list, zone_subdomain, stamp)
update = process_zone(zone, stamp, zone_timeout, sync_zone)
if update:
log_update_zone(update)
success = ovh_update_zone(zone_domain, zone, update, sync_zone)
livebox_update_fw(zone, update, sync_zone)
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,15 +13,22 @@ mypc1
mypc2
mypc3
[NatHosts]
mypc2
mypc4
[PortsNat]
mypc2 = 22,443
mypc3 = 10022
[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

2
ovh

Submodule ovh updated: 526c9e0fab...11d03357a5

View File

@ -1,8 +1,13 @@
#!/usr/bin/python3
import sys
from ovh import ovh
domain = 'example.com'
if len(sys.argv) != 2:
print('usage: {} <domain>'.format(sys.argv[0]))
sys.exit(1)
domain = sys.argv[1]
client = ovh.Client()

2
sysbus

Submodule sysbus updated: bf7689ce0a...5526cb5364