From bb4055c87fa1746fac6035fd4be0ce1d8c91b52f Mon Sep 17 00:00:00 2001 From: brent s Date: Tue, 12 May 2020 18:13:42 -0400 Subject: [PATCH] rewrite done --- utils/he_ipv6.py | 307 ------------------ utils/he_ipv6/__init__.py | 1 - utils/he_ipv6/args.py | 11 +- ...e.he_ipv6.xml => example.tunnelbroker.xml} | 0 utils/he_ipv6/tunnelbroker.py | 53 ++- utils/{he_ipv6/main.py => manage_heipv6.py} | 14 +- 6 files changed, 63 insertions(+), 323 deletions(-) delete mode 100755 utils/he_ipv6.py rename utils/he_ipv6/{example.he_ipv6.xml => example.tunnelbroker.xml} (100%) rename utils/{he_ipv6/main.py => manage_heipv6.py} (70%) diff --git a/utils/he_ipv6.py b/utils/he_ipv6.py deleted file mode 100755 index bdbb6bc..0000000 --- a/utils/he_ipv6.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import configparser -import ipaddress -import logging -import logging.handlers -import os -import socket -import sys -## -import requests -import requests.auth -from pyroute2 import IPRoute -try: - # https://www.freedesktop.org/software/systemd/python-systemd/journal.html#journalhandler-class - from systemd import journal - _has_journald = True -except ImportError: - _has_journald = False - - -# TODO: add checking to see if we're already configured. - - -# https://wiki.archlinux.org/index.php/IPv6_tunnel_broker_setup -# https://forums.he.net/index.php?topic=3153.0 -# https://gist.github.com/pklaus/960672 -# https://shorewall.org/6to4.htm#idm143 - - -logfile = '/var/log/tunnelbroker_manager.log' -# Prep the log file. -logfile = os.path.abspath(os.path.expanduser(logfile)) -os.makedirs(os.path.dirname(logfile), exist_ok = True, mode = 0o0700) -if not os.path.isfile(logfile): - with open(logfile, 'w') as fh: - fh.write('') -os.chmod(logfile, 0o0600) - -# And set up logging. -_cfg_args = {'handlers': [], - 'level': logging.DEBUG} -if _has_journald: - # There were some weird changes somewhere along the line. - try: - # But it's *probably* this one. - h = journal.JournalHandler() - except AttributeError: - h = journal.JournaldLogHandler() - # Systemd includes times, so we don't need to. - h.setFormatter(logging.Formatter(style = '{', - fmt = ('{name}:{levelname}:{name}:{filename}:' - '{funcName}:{lineno}: {message}'))) - _cfg_args['handlers'].append(h) -h = logging.handlers.RotatingFileHandler(logfile, - encoding = 'utf8', - # Disable rotating for now. - # maxBytes = 50000000000, - # backupCount = 30 - ) -h.setFormatter(logging.Formatter(style = '{', - fmt = ('{asctime}:' - '{levelname}:{name}:{filename}:' - '{funcName}:{lineno}: {message}'))) -_cfg_args['handlers'].append(h) -logging.basicConfig(**_cfg_args) -logger = logging.getLogger('HE Tunnelbroker Manager') -logger.info('Logging initialized.') - - -class TunnelBroker(object): - ipget_url = 'https://ipv4.clientinfo.square-r00t.net/' - ipget_params = {'raw': '1'} - api_base = 'https://ipv4.tunnelbroker.net/nic/update' - - def __init__(self, conf, tun_id = None, wan_ip = True, update = True, *args, **kwargs): - self.my_ip = None - self.user = None - self.update_key = None - self.ipr = None - self.iface_idx = None - self.wan = wan_ip - self.force_update = update - self.conf_file = os.path.abspath(os.path.expanduser(conf)) - logger.debug('Using config path: {0}'.format(self.conf_file)) - self._conf = configparser.ConfigParser(allow_no_value = True, - interpolation = configparser.ExtendedInterpolation()) - try: - self._conf.read(self.conf_file) - logger.debug('Read in configuration successfully.') - except Exception as e: - logger.error('Failed reading configuration: {0}'.format(e)) - raise e - if len(self._conf.sections()) < 1: - logger.error('Config file has no sections/tunnels defined.') - raise RuntimeError('Config file has no sections/tunnels defined') - self.tun_id = tun_id - if self.tun_id and self.tun_id not in self._conf.sections(): - logger.error(('The tun_id {0} is not a valid tunnel ID. ' - 'Valid tunnels are: {1}').format(self.tun_id, ', '.join(self._conf.sections()))) - raise ValueError('tun_id not a valid tunnel ID') - elif not self.tun_id: - self.tun_id = self._conf.sections()[0] - logger.debug('Automatically set tun_id to {0}'.format(self.tun_id)) - self.cfg = self._conf[self.tun_id] - self.server = ipaddress.ip_address(self.cfg['server']) - logger.debug('Set server IP to {0}.'.format(str(self.server))) - self.allocations = [ipaddress.ip_network(ip.strip()) for ip in self.cfg['allocations'].split(',')] - logger.debug('Using address allocations: {0}'.format(', '.join([str(ip) for ip in self.allocations]))) - _net = ipaddress.ip_network(self.cfg['address'].strip(), strict = False) - self.addr = ipaddress.ip_address(self.cfg['address'].strip().split('/')[0]) - self.network = ipaddress.ip_network(self.cfg['address'].strip(), strict = False) - for k in ('user', 'update_key'): - setattr(self, k, self.cfg[k]) - # Don't log creds, even in debug. - # logger.debug('Set {0} to {1}'.format(k, getattr(self, k))) - self.iface_name = 'he-{0}'.format(self.tun_id) - - def _get_my_ip(self): - if self.wan: - logger.debug('WAN IP tunneling enabled; fetching WAN IP.') - req = requests.get(self.ipget_url, params = self.ipget_params) - if not req.ok: - logger.error('Could not fetch self IP. Request returned {0}.'.format(req.status_code)) - raise RuntimeError('Could not fetch self IP') - self.my_ip = ipaddress.ip_address(req.json()['ip']) - logger.debug('Set my_ip to {0}.'.format(str(self.my_ip))) - else: - logger.debug('WAN IP tunneling disabled; fetching LAN IP.') - if not self.ipr: - self.ipr = IPRoute() - _defrt = self.ipr.get_default_routes(family = socket.AF_INET) - if len(_defrt) != 1: # This (probably) WILL fail on multipath systems. - logger.error('Could not determine default route. Does this machine have a single default route?') - raise RuntimeError('Could not determine default IPv4 route') - self.my_ip = ipaddress.ip_address(_defrt[0]['attrs']['RTA_PREFSRC']) - logger.debug('Set my_ip to {0}.'.format(str(self.my_ip))) - return(None) - - def start(self): - if self.force_update: - logger.debug('IP update forced; updating.') - self._get_my_ip() - self.update() - if not self.ipr: - self.ipr = IPRoute() - try: - self.ipr.link('add', - ifname = self.iface_name, - kind = 'sit', - sit_local = str(self.my_ip), - sit_remote = str(self.server), - sit_ttl = 255) - logger.debug('Added link {0} successfully.'.format(self.iface_name)) - except Exception as e: - logger.error('Could not create link for link {0} ' - '(maybe it already exists?): {1}'.format(self.iface_name, e)) - raise e - try: - self.iface_idx = self.ipr.link_lookup(ifname = self.iface_name)[0] - logger.debug('Found link {0} at index {1}.'.format(self.iface_name, self.iface_idx)) - except Exception as e: - logger.error('Could not set iface_idx for iface name {0}: {1}'.format(self.iface_name, e)) - raise e - try: - self.ipr.link('set', index = self.iface_idx, state = 'up', mtu = 1480) - logger.debug('Set link {0} status to UP.'.format(self.iface_name)) - except Exception as e: - logger.error(('Could not bring up link for iface name {0} at index {1}: ' - '{2}').format(self.iface_name, self.iface_idx, e)) - raise e - try: - self.ipr.addr('add', - index = self.iface_idx, - address = str(self.addr), - mask = self.network.prefixlen, - family = socket.AF_INET6) - logger.debug('Added address {0} to link {1} with prefix {2}.'.format(str(self.addr), - self.iface_name, - self.network.prefixlen)) - except Exception as e: - logger.error(('Could not add address {0} on link {1}: ' - '{2}').format(str(self.addr), self.iface_name, e)) - raise e - for a in self.allocations: - try: - _addr = next(a.hosts()) - self.ipr.addr('add', - index = self.iface_idx, - address = str(_addr), - mask = a.prefixlen, - family = socket.AF_INET6) - logger.debug('Added address {0} to link {1} with prefix {2}.'.format(str(_addr), - self.iface_name, - a.prefixlen)) - except Exception as e: - logger.error(('Could not add address {0} on link {1}: ' - '{2}').format(str(self.addr), self.iface_name, e)) - raise e - try: - self.ipr.route('add', dst = 'default', oif = self.iface_idx, family = socket.AF_INET6) - logger.debug('Added default route for link {0}.'.format(self.iface_name)) - except Exception as e: - logger.error(('Could not add default IPv6 route on link {0}: {1}').format(self.iface_name, e)) - raise e - self.ipr.close() - return(None) - - def stop(self): - if not self.ipr: - self.ipr = IPRoute() - try: - self.iface_idx = self.ipr.link_lookup(ifname = self.iface_name)[0] - logger.debug('Found link {0} at index {1}.'.format(self.iface_name, self.iface_idx)) - except Exception as e: - logger.error('Could not set iface_idx for link {0}: {1}'.format(self.iface_name, e)) - raise e - try: - self.ipr.route('del', dst = 'default', oif = self.iface_idx, family = socket.AF_INET6) - logger.debug('Removed default route for link {0}.'.format(self.iface_name)) - except Exception as e: - logger.error(('Could not remove default IPv6 route on link {0}: ' - '{1} (continuing anyways)').format(self.iface_name, e)) - try: - self.ipr.link('set', index = self.iface_idx, state = 'down') - except Exception as e: - logger.error('Could not bring down link {0}: {1} (continuing anyways)'.format(self.iface_name, e)) - try: - self.ipr.link('del', index = self.iface_idx) - logger.debug('Deleted link {0}.'.format(self.iface_name)) - except Exception as e: - logger.error('Could not delete link {0}: {1}'.format(self.iface_name, e)) - raise e - self.ipr.close() - return(None) - - def update(self, oneshot = False): - self._get_my_ip() - auth_handler = requests.auth.HTTPBasicAuth(self.user, self.update_key) - logger.debug('Set auth handler.') - logger.debug('Requesting IP update at provider.') - req = requests.get(self.api_base, - params = {'hostname': self.tun_id, - 'myip': self.my_ip}, - auth = auth_handler) - if not req.ok: - logger.error('Could not update IP at provider. Request returned {0}.'.format(req.status_code)) - raise RuntimeError('Could not update client IP in tunnel') - status = req.content.decode('utf-8').split()[0].strip() - if status.lower() not in ('good', 'nochg'): - logger.error('Returned following failure message: {0}'.format(req.content.decode('utf-8'))) - raise RuntimeError('Client IP update returned failure') - else: - logger.debug('Returned success message: {0}'.format(req.content.decode('utf-8'))) - if self.ipr and oneshot: - self.ipr.close() - self.ipr = None - return(None) - - -def parseArgs(): - args = argparse.ArgumentParser(description = ('Dynamically update and enable/disable ' - 'Hurricane Electric Tunnelbroker')) - args.add_argument('-i', '--no-wan-ip', - dest = 'wan_ip', - action = 'store_false', - help = ('If specified, use the RFC1918 IP address assigned to this machine instead of the WAN ' - 'IP (necessary if this machine is behind NAT)')) - args.add_argument('-c', '--config', - dest = 'conf', - default = '~/.config/he_tunnelbroker.ini', - help = ('The path to the config. ' - 'Default: ~/.config/he_tunnelbroker.ini')) - args.add_argument('-t', '--tunnel-id', - dest = 'tun_id', - help = ('The tunnel profile ID/name to use in -c/--config. ' - 'Default is to use the first one found.')) - args.add_argument('-u', '--no-update', - dest = 'update', - action = 'store_false', - help = ('If specified, do not perform the automatic update for start operations. Has no effect ' - 'for other operations')) - args.add_argument('oper', - metavar = 'OPERATION', - choices = ('update', 'start', 'stop'), - help = ('The operation to perform ("start", "stop", or "update"). "update" is performed ' - 'automatically by "start", but otherwise will just update the IPv4 address on record ' - 'at tunnelbroker')) - return(args) - - -def main(): - args = parseArgs().parse_args() - logger.debug('Invoked with args: {0}'.format(vars(args))) - tb = TunnelBroker(**vars(args)) - if args.oper == 'start': - tb.start() - elif args.oper == 'stop': - tb.stop() - elif args.oper == 'update': - tb.update(oneshot = True) - return(None) - - -if __name__ == '__main__': - main() diff --git a/utils/he_ipv6/__init__.py b/utils/he_ipv6/__init__.py index eab7d37..0ca8cd6 100644 --- a/utils/he_ipv6/__init__.py +++ b/utils/he_ipv6/__init__.py @@ -2,4 +2,3 @@ from . import args from . import config from . import logger from . import tunnelbroker -from . import main diff --git a/utils/he_ipv6/args.py b/utils/he_ipv6/args.py index 08d38c5..116c613 100644 --- a/utils/he_ipv6/args.py +++ b/utils/he_ipv6/args.py @@ -10,14 +10,15 @@ def parseArgs(): help = ('If specified, use the RFC1918 IP address assigned to this machine instead of the WAN ' 'IP (necessary if this machine is behind NAT)')) args.add_argument('-c', '--config', - dest = 'conf', - default = '~/.config/he_tunnelbroker.ini', - help = ('The path to the config. ' - 'Default: ~/.config/he_tunnelbroker.ini')) + dest = 'conf_xml', + default = '~/.config/he_tunnelbroker.xml', + help = ('The path to the config. See example.tunnelbroker.xml' + 'Default: ~/.config/he_tunnelbroker.xml')) args.add_argument('-t', '--tunnel-id', dest = 'tun_id', + type = int, help = ('The tunnel profile ID/name to use in -c/--config. ' - 'Default is to use the first one found.')) + 'Default is to use the first one found')) args.add_argument('-u', '--no-update', dest = 'update', action = 'store_false', diff --git a/utils/he_ipv6/example.he_ipv6.xml b/utils/he_ipv6/example.tunnelbroker.xml similarity index 100% rename from utils/he_ipv6/example.he_ipv6.xml rename to utils/he_ipv6/example.tunnelbroker.xml diff --git a/utils/he_ipv6/tunnelbroker.py b/utils/he_ipv6/tunnelbroker.py index 8a556d6..6f01e41 100644 --- a/utils/he_ipv6/tunnelbroker.py +++ b/utils/he_ipv6/tunnelbroker.py @@ -4,6 +4,7 @@ import socket logger = logging.getLogger() ## import requests +import requests.auth from pyroute2 import IPRoute ## from . import config @@ -13,6 +14,8 @@ class TunnelBroker(object): url_ip = 'https://ipv4.clientinfo.square-r00t.net/' params_ip = {'raw': '1'} url_api = 'https://ipv4.tunnelbroker.net/nic/update' + # def_rt = 'default' + def_rt = '::192.88.99.1' def __init__(self, conf_xml, tun_id = None, wan_ip = True, update = True, *args, **kwargs): self.conf_file = os.path.abspath(os.path.expanduser(conf_xml)) @@ -111,8 +114,7 @@ class TunnelBroker(object): ipr.close() raise e try: - # ipr.route('add', dst = 'default', oif = self.iface_idx, family = socket.AF_INET6) - ipr.route('add', dst = '::192.88.99.1', oif = self.iface_idx, family = socket.AF_INET6) + ipr.route('add', dst = self.def_rt, oif = self.iface_idx, family = socket.AF_INET6) logger.debug('Added default route for link {0}.'.format(self.iface_name)) except Exception as e: logger.error(('Could not add default IPv6 route on link {0}: {1}').format(self.iface_name, e)) @@ -122,5 +124,50 @@ class TunnelBroker(object): return(None) def stop(self): + ipr = IPRoute() + try: + self.iface_idx = ipr.link_lookup(ifname = self.iface_name)[0] + logger.debug('Found link {0} at index {1}.'.format(self.iface_name, self.iface_idx)) + except Exception as e: + ipr.close() + logger.error('Could not set iface_idx for link {0}: {1}'.format(self.iface_name, e)) + raise e + try: + ipr.route('del', dst = self.def_rt, oif = self.iface_idx, family = socket.AF_INET6) + logger.debug('Removed default route for link {0}.'.format(self.iface_name)) + except Exception as e: + logger.error(('Could not remove default IPv6 route on link {0}: ' + '{1} (continuing anyways)').format(self.iface_name, e)) + try: + ipr.link('set', index = self.iface_idx, state = 'down') + except Exception as e: + logger.error('Could not bring down link {0}: {1} (continuing anyways)'.format(self.iface_name, e)) + try: + ipr.link('del', index = self.iface_idx) + logger.debug('Deleted link {0}.'.format(self.iface_name)) + except Exception as e: + logger.error('Could not delete link {0}: {1}'.format(self.iface_name, e)) + ipr.close() + raise e + ipr.close() + return(None) - def update(self): + def update(self, oneshot = False): + self._get_my_ip() + auth_handler = requests.auth.HTTPBasicAuth(self.tun.creds.user, self.tun.creds.key) + logger.debug('Set auth handler.') + logger.debug('Requesting IP update at provider.') + req = requests.get(self.url_api, + params = {'hostname': str(self.tun.id), + 'myip': self.my_ip}, + auth = auth_handler) + if not req.ok: + logger.error('Could not update IP at provider. Request returned {0}.'.format(req.status_code)) + raise RuntimeError('Could not update client IP in tunnel') + status = req.content.decode('utf-8').split()[0].strip() + if status.lower() not in ('good', 'nochg'): + logger.error('Returned following failure message: {0}'.format(req.content.decode('utf-8'))) + raise RuntimeError('Client IP update returned failure') + else: + logger.debug('Returned success message: {0}'.format(req.content.decode('utf-8'))) + return(None) diff --git a/utils/he_ipv6/main.py b/utils/manage_heipv6.py similarity index 70% rename from utils/he_ipv6/main.py rename to utils/manage_heipv6.py index 1f3be5f..5c823da 100755 --- a/utils/he_ipv6/main.py +++ b/utils/manage_heipv6.py @@ -1,16 +1,16 @@ +#!/usr/bin/env python3 + +import he_ipv6 + + import logging -## -from . import args -from . import tunnelbroker - - logger = logging.getLogger() def main(): - _args = args.parseArgs().parse_args() + _args = he_ipv6.args.parseArgs().parse_args() logger.debug('Invoked with args: {0}'.format(vars(_args))) - tb = tunnelbroker.TunnelBroker(**vars(_args)) + tb = he_ipv6.tunnelbroker.TunnelBroker(**vars(_args)) if _args.oper == 'start': tb.start() elif _args.oper == 'stop':