Compare commits

33 Commits

Author SHA1 Message Date
gilles 31f5e94ae1 livebox now longer answer to ping6 on LAN :( 2022-01-14 19:02:07 +01:00
gilles 60b13d46f7 add some error checkings for livebox req timeouts 2022-01-14 18:57:27 +01:00
gilles f0033fb61a update sysbus 2021-09-28 15:35:56 +02:00
gilles b1a296f05a zone update fixes 2021-04-21 17:04:38 +02:00
gilles ff4302cf0a bug fix in address expiration 2020-10-08 11:24:43 +02:00
gilles a63bd3a5a5 log: stdout if on tty, in logfile otherwise 2020-10-04 22:56:05 +02:00
gilles 2f970cd71c add livebox firewall support 2020-10-04 17:41:05 +02:00
gilles 677db2e9a6 get hosts list with both IPv6 and IPv4 addresses 2020-10-04 15:46:01 +02:00
gilles 893327c31d sync ovh zone is zone.json does not exists 2020-10-04 15:25:16 +02:00
gilles a30efabbda clean 2020-10-04 15:02:56 +02:00
gilles 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
gilles f4652bfc12 ovh update 2020-10-04 11:11:31 +02:00
gilles 2534a41a27 sysbus update 2020-10-04 11:11:26 +02:00
gilles 819df7f6e1 support new sysbus layout 2020-02-05 17:52:43 +01:00
gilles 8c4f998018 bug fix host alias list 2020-01-03 15:53:16 +01:00
gilles 055f4949cd filter out invalid 0.0.0.0 ipv4 2019-12-17 13:58:54 +01:00
gilles 79337e8411 document MailIgnore 2019-12-16 18:51:15 +01:00
gilles 11c20250b9 MailIgnore section can contains simple hostnames
it's no longer needed to put host.subdomain
2019-12-16 18:40:41 +01:00
gilles 0616e22e0c convert hostnames to lowercase 2019-12-16 18:31:33 +01:00
gilles 79eb79f46c catch ovh_update errors 2019-11-12 12:23:26 +01:00
gilles 5aa8575127 ovh update fixes 2019-11-12 12:20:50 +01:00
gilles 6894ee25d9 typo fix 2019-11-12 12:19:51 +01:00
gilles 7fb17678b8 debug update 2019-11-12 12:19:27 +01:00
gilles 2609ce5f6d Add MailIgnore option support 2019-10-10 22:49:41 +02:00
gilles 7599ec2684 fixed multiple entries ovh update 2019-10-10 10:55:00 +02:00
gilles bdcf586a6c add support for multiple addresses in zone.list 2019-10-10 10:55:00 +02:00
gilles 95a20f78e5 git ignore log files 2019-10-10 10:55:00 +02:00
gilles 67335c198b register_ovh_account: pass domain as argument 2019-10-10 10:55:00 +02:00
gilles 306d4b536e cosmetic log update 2019-10-10 10:55:00 +02:00
gilles df66e50d47 add [Aliases] support 2019-10-10 10:55:00 +02:00
gilles 841e6043c6 only update ovh zone with diffs 2019-10-05 23:03:38 +02:00
gilles 2cc7d966a0 fix ipv6 sysbus request 2019-10-05 23:01:08 +02:00
gilles 6c4bc82319 sysbus: update 2019-10-05 19:33:59 +02:00
9 changed files with 403 additions and 154 deletions
+2 -4
View File
@@ -1,6 +1,4 @@
cron.log *.log
home.conf home.conf
ovh.conf ovh.conf
ovh.conf.old zone.json
zone.list
zone.log
+6
View File
@@ -0,0 +1,6 @@
[submodule "sysbus"]
path = sysbus
url = https://github.com/rene-d/sysbus.git
[submodule "ovh"]
path = ovh
url = https://github.com/ovh/python-ovh.git
+4
View File
@@ -143,6 +143,10 @@ Setup - Réseau local
Chaque mise à jour génerera un mail qui sera envoyé à cette adresse. 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 Utilisation
----------- -----------
+371 -103
View File
@@ -1,25 +1,29 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys
import os
import configparser import configparser
import urllib.request import copy
import urllib.parse import json
import socket import os
import smtplib
import subprocess import subprocess
import sys
import time import time
from email.message import EmailMessage
from pprint import pprint
rundir=os.path.realpath(os.path.dirname(sys.argv[0])) rundir=os.path.realpath(os.path.dirname(sys.argv[0]))
os.chdir(rundir) os.chdir(rundir)
sys.path.append(os.path.join(rundir,'sysbus/src'))
from sysbus import sysbus
from ovh import ovh from ovh import ovh
def load_conf(): def load_conf():
global zone_filename, log_filename global zone_filename, log_filename
global wan_hostname global wan_hostname, zone_domain, zone_subdomain, zone_timeout
global hosts6_list, hosts4_list global hosts_list, nat_list, pinhole_list
global zone_domain, zone_subdomain global mail_from, mail_to, mail_ignore_list
conf = configparser.ConfigParser(allow_no_value=True) conf = configparser.ConfigParser(allow_no_value=True)
conf.read('home.conf') conf.read('home.conf')
@@ -27,119 +31,383 @@ def load_conf():
zone_filename = conf['Files']['zonefile'] zone_filename = conf['Files']['zonefile']
log_filename = conf['Files']['logfile'] log_filename = conf['Files']['logfile']
wan_hostname = conf['Wan']['hostname'] 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 ]
hosts6_list = [ host for host in conf['Hosts'] ] hosts_list = [ host.lower() for host in conf['Hosts'] ]
hosts4_list = [ host for host in conf['NatHosts'] ]
zone_domain = conf['Zone']['domain'] nat_list = {}
zone_subdomain = conf['Zone']['subdomain'] 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 get_host_ipv6(name): def ping(hostname):
cmd = "ping -4 -c1 -w3 %s" % hostname
ret = subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return 0 if ret.returncode else 1
def get_hosts():
r = sysbus.requete('Devices:get', silent=True)
hosts = { 'A': {}, 'AAAA': {} }
if not r:
return hosts
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():
r = sysbus.requete('NMC:getWANStatus', silent=True)
wan = dict()
if not r or not r.get('data') or not r['data'].get('IPAddress') or not r['data'].get('IPv6Address'):
log('get_wan_addr: {}'.format(r))
return None
wan['ipv4'] = r['data']['IPAddress']
wan['ipv6'] = r['data']['IPv6Address']
if wan['ipv4'] == '0.0.0.0':
wan['ipv4'] = ''
return wan
def full_name(host, domain):
host = host.replace('_', '-')
return '.'.join([host, domain])
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_add_entry(zone, 'A', wan_hostname, wan_addr['ipv4'], stamp)
for host in hosts_nat:
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_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 process_zone(zone, stamp, grace_period, sync_zone):
update = { 'add': [], 'delete': [] }
for prot in zone:
for name in zone[prot]:
active = False
for addr in zone[prot][name]:
e = zone[prot][name][addr]
if e['first'] == stamp:
update['add'].append([prot, name, addr])
if e['last'] == stamp:
active = True
for addr in zone[prot][name]:
e = zone[prot][name][addr]
if active and e['last'] < stamp:
update['delete'].append([prot, name, addr])
elif not active 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 read_zone_list(zone_filename):
try: try:
res = socket.getaddrinfo(name, None, socket.AF_INET6, socket.SOCK_STREAM) with open(zone_filename) as jsonfile:
except socket.gaierror: zone = json.load(jsonfile)
return [] return zone
ips = [ r[4][0] for r in res ]
return ips
def get_wan_ipv4():
try:
f = urllib.request.urlopen('https://ipv4.wtfismyip.com/text')
return f.read().decode('utf-8').split('\n')[0]
except urllib.error.URLError as e:
print(e)
sys.exit(0)
def get_wan_ipv6():
return get_host_ipv6(wan_hostname)[0]
def ping_alive(ip):
cmd = ['fping6', '-q', '-B1', ip]
ret = subprocess.call(cmd)
if not ret:
return True
return False
def make_zone_list(wan_ipv4, hosts4, hosts6, subdomain):
zone = []
for host in hosts4:
entry = [ host+'.'+subdomain, 'A', wan_ipv4 ]
zone.append(entry)
for host in hosts6:
for ip in get_host_ipv6(host):
if ping_alive(ip):
entry = [ host+'.'+subdomain, 'AAAA', ip ]
zone.append(entry)
zone.sort()
return zone
def read_zone_list_from_file(zone_filename):
zone = []
try:
with open(zone_filename, 'r') as zf:
for line in zf:
line = line.rstrip('\n')
entry = line.split(' ')
if entry[1] not in ['A', 'AAAA']:
continue
zone.append(entry)
except FileNotFoundError: except FileNotFoundError:
pass return {}
return zone
def write_zone_list_to_file(zone_filename, zone_list): def write_zone_list(zone_filename, zone, update):
with open(zone_filename, 'w') as zf: zone = copy.deepcopy(zone)
for entry in zone_list: if update:
line = ' '.join(entry)+'\n' for prot, name, addr in update['delete']:
zf.write(line) 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 ovh_update_zone(domain, prev_zone_list, zone_list):
if not len(zone_list) and not len(remove_zone_list): def log(msg):
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
if sys.stdout.isatty():
for line in msg.split('\n'):
print("%s - %s" % (stamp, msg))
else:
with open(log_filename, 'a') as logfile:
for line in msg.split('\n'):
logfile.write("%s - %s\n" % (stamp, msg))
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, 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:
log('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:
log("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
log("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)
log("OVH: Refresh zone %s" % domain)
client.post('/domain/zone/%s/refresh' % domain)
return True
except:
log('OVH update error\n')
return False return False
client = ovh.Client()
print("\ndate:", time.strftime("%c")) def livebox_rule_id(words):
for host, fieldtype, target in prev_zone_list: id='dyndomain_{}'.format('_'.join(words))
result = client.get('/domain/zone/%s/record' % domain, id = id.replace('.', '_')
fieldType=fieldtype, return id
subDomain=host)
for id in result:
print("Deleting old entry for %s %s" % (host, fieldtype))
client.delete('/domain/zone/%s/record/%ld' % (domain, id))
for host, fieldtype, target in zone_list: def livebox_delete_port_nat(port, addr):
print("Create new entry for %s %s %s" % (host, fieldtype, target)) log('livebox: delete PortNat {} to {}'.format(port, addr))
client.post('/domain/zone/%s/record' % domain, id = livebox_rule_id([port])
fieldType=fieldtype, r = sysbus.requete('Firewall:deletePortForwarding', { 'id': id, 'origin': 'webui' }, silent=True)
subDomain=host,
target=target,
ttl=60)
print("Refresh zone %s" % domain) def livebox_delete_pinhole(port, addr):
client.post('/domain/zone/%s/refresh' % domain) log('livebox: delete pinhole {} to {}'.format(port, addr))
return True id = livebox_rule_id([port, addr])
r = sysbus.requete('Firewall:deletePinhole', { 'id': id, 'origin': 'webui' }, silent=True)
def livebox_add_port_nat(port, addr):
log('livebox: add PortNat {} to {}'.format(port, addr))
id = livebox_rule_id([port])
a = {
'id': id,
'origin': 'webui',
'sourceInterface': 'data',
'destinationIPAddress': addr,
'protocol': '6,17',
'internalPort': port,
'enable': True,
}
r = sysbus.requete('Firewall:setPortForwarding', a, silent=True)
def livebox_add_pinhole(port, addr):
log('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,17',
'ipversion': '6',
'enable': True,
}
r = sysbus.requete('Firewall:setPinhole', a, silent=True)
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, 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
log('Send email to %s' % mail_to)
msg = EmailMessage()
msg['Subject'] = "Livebox update in %s" % zone_domain
msg['From'] = mail_from
msg['To' ] = mail_to
txt = "Livebox update\n\n"
if updated['add']:
txt = txt + "\nUpdated entries:\n"
for prot,name,addr in update['add']:
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
if update['delete']:
txt = txt + "\nRemoved entries:\n"
for prot,name,addr in update['delete']:
txt = txt + " %-20s %-4s %s\n" % (name,prot,addr)
txt = txt + '\n'
txt = txt + "WAN IPv4 : %s\n" % wan['ipv4']
txt = txt + "WAN IPv6 : %s\n" % wan['ipv6']
msg.set_content(txt)
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
load_conf() load_conf()
hosts4_list.append(wan_hostname)
hosts6_list.append(wan_hostname)
wan_ipv4 = get_wan_ipv4() if not ping(wan_hostname):
wan_ipv6 = get_wan_ipv6() log("%s is down" % wan_hostname)
sys.exit(0)
zone_list = make_zone_list(wan_ipv4, hosts4_list, hosts6_list, zone_subdomain) sysbus.load_conf()
prev_zone_list = read_zone_list_from_file(zone_filename) try:
r = sysbus.auth(False)
if not r:
log('Error: cannot authenticate on livebox')
sys.exit(1)
except Exception as e:
log('Error: %s'.format(e))
sys.exit(1)
if zone_list != prev_zone_list: hosts = get_hosts()
success = ovh_update_zone(zone_domain, prev_zone_list, zone_list) wan = get_wan_addr()
write_zone_list_to_file(zone_filename, zone_list) if not wan:
sys.exit(0)
zone = read_zone_list(zone_filename)
sync_zone = False
if not zone:
sync_zone = True
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:
try:
send_update_mail(mail_to, mail_from, zone_domain, update, mail_ignore_list, wan)
except:
pass
write_zone_list(zone_filename, zone, update)
+11 -4
View File
@@ -2,7 +2,7 @@
# edit it and save it as home.conf # edit it and save it as home.conf
[Files] [Files]
zonefile = zone.list zonefile = zone.json
logfile = zone.log logfile = zone.log
[Wan] [Wan]
@@ -13,15 +13,22 @@ mypc1
mypc2 mypc2
mypc3 mypc3
[NatHosts] [PortsNat]
mypc2 mypc2 = 22,443
mypc4 mypc3 = 10022
[Firewall]
mypc2 = 22,443
mypc3 = 10022
[Zone] [Zone]
domain = example.com domain = example.com
subdomain = home subdomain = home
keep = 3d
[Mail] [Mail]
from = me@example.com from = me@example.com
to = you@example.com to = you@example.com
[MailIgnore]
mypc2
-40
View File
@@ -1,40 +0,0 @@
get ipv4 wan
------------
$ curl -4 http://ifconfig.co
90.116.135.67
[please limits to 1 req/min]
$ curl https://ipinfo.io/ip
90.116.135.67
$ curl https://ipv4.wtfismyip.com/text
90.116.135.67
[1 request per minute]
get ipv6 wan
------------
$ host livebox.home
livebox.home has address 192.168.1.1
livebox.home has IPv6 address 2a01:cb1d:164:f500:327c:b2ff:fe9e:cf16
get ipv6 host
-------------
$ host arwen.home
arwen.home has address 192.168.1.113
arwen.home has IPv6 address 2a01:cb1d:164:f500:8ff:3eea:a3b2:714e
$ host arwen.home
arwen.home has address 192.168.1.113
arwen.home has IPv6 address 2a01:cb1d:164:f500:3df0:c245:7d7a:26e2
arwen.home has IPv6 address 2a01:cb1d:164:f500:8ff:3eea:a3b2:714e
arwen.home has IPv6 address 2a01:cb1d:164:f500:28af:1644:6308:f13
$ fping6 2a01:cb1d:164:f500:8ff:3eea:a3b2:714e 2a01:cb1d:164:f500:3df0:c245:7d7a:26e2 2a01:cb1d:164:f500:28af:1644:6308:f13
2a01:cb1d:164:f500:8ff:3eea:a3b2:714e is alive
2a01:cb1d:164:f500:3df0:c245:7d7a:26e2 is unreachable
2a01:cb1d:164:f500:28af:1644:6308:f13 is unreachable
+1 -1
Submodule ovh updated: 526c9e0fab...11d03357a5
+7 -2
View File
@@ -1,14 +1,19 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys
from ovh import ovh 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() client = ovh.Client()
ck = client.new_consumer_key_request() ck = client.new_consumer_key_request()
ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domain/zone/%s/record' % domain) ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domain/zone/%s/record' % domain)
ck.add_rules(ovh.API_READ_WRITE, '/domain/zone/%s/record/*' % domain) ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domain/zone/%s/record/*' % domain)
ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domain/zone/%s/refresh' % domain) ck.add_rules(ovh.API_READ_WRITE_SAFE, '/domain/zone/%s/refresh' % domain)
validation = ck.request() validation = ck.request()
Submodule
+1
Submodule sysbus added at 87040a0906