import datetime import json import logging import os import socket logger = logging.getLogger() ## import requests import requests.auth from pyroute2 import IPRoute ## from . import config from . import tunnel class TunnelBroker(object): url_ip = 'https://ipv4.clientinfo.square-r00t.net/' params_ip = {'raw': '1'} url_api = 'https://ipv4.tunnelbroker.net/nic/update' ip_cache = '~/.cache/he_tunnelbroker.my_ip.json' 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)) logger.debug('Using config path: {0}'.format(self.conf_file)) self._conf = config.Config(self.conf_file) if tun_id: self.tun = self._conf.tunnels[int(tun_id)] else: tun_id = list(self._conf.tunnels.keys())[0] self.tun = self._conf.tunnels[tun_id] self.iface_name = 'he-{0}'.format(self.tun.id) self.wan = wan_ip self.needs_update = False self.force_update = update self.ip_cache = os.path.abspath(os.path.expanduser(self.ip_cache)) self.cached_ips = [] self.my_ip = None self.iface_idx = None def _get_my_ip(self): if os.path.isfile(self.ip_cache): with open(self.ip_cache, 'r') as fh: self.cached_ips = [(datetime.datetime.fromtimestamp(i[0]), tunnel.IP4(i[1], 32)) for i in json.loads(fh.read())] else: os.makedirs(os.path.dirname(self.ip_cache), exist_ok = True, mode = 0o0700) if self.wan: logger.debug('WAN IP tunneling enabled; fetching WAN IP.') req = requests.get(self.url_ip, params = self.params_ip) 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 = tunnel.IP4(req.json()['ip'], 32) logger.debug('Set my_ip to {0}.'.format(self.my_ip.str)) else: logger.debug('WAN IP tunneling disabled; fetching LAN IP.') ipr = IPRoute() _defrt = 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 = tunnel.IP4(_defrt[0]['attrs']['RTA_PREFSRC'], 32) ipr.close() logger.debug('Set my_ip to {0}.'.format(self.my_ip.str)) chk_tuple = (datetime.datetime.utcnow(), self.my_ip) if len(self.cached_ips) >= 1 and self.my_ip.str != self.cached_ips[-1][1].str: self.needs_update = True elif len(self.cached_ips) == 0: self.needs_update = True if self.needs_update: self.cached_ips.append(chk_tuple) with open(self.ip_cache, 'w') as fh: fh.write(json.dumps([(i[0].timestamp(), i[1].str) for i in self.cached_ips], indent = 4)) return(None) def start(self): self._get_my_ip() if any((self.force_update, self.needs_update)): logger.debug('IP update forced or needed; updating.') self.update() logger.debug('Attempting to clean up any pre-existing config') try: self.stop() logger.debug('Pre-existing config removed, continuing') except Exception as e: logger.debug('Config seems to not exist, continuing') ipr = IPRoute() try: ipr.link('add', ifname = self.iface_name, kind = 'sit', sit_local = self.my_ip.str, sit_remote = self.tun.endpoint.str, 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?) with local {1} and remote {2}: {3}'.format(self.iface_name, self.my_ip.str, self.tun.endpoint.str, e)) ipr.close() raise e 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: logger.error('Could not set iface_idx for iface name {0}: {1}'.format(self.iface_name, e)) ipr.close() raise e try: ipr.addr('add', index = self.iface_idx, address = self.tun.client.str, mask = self.tun.client.prefix, family = socket.AF_INET6) logger.debug('Added address {0} to link {1} with prefix {2}.'.format(self.tun.client.str, self.iface_name, self.tun.client.prefix)) except Exception as e: logger.error(('Could not add address {0} on link {1}: ' '{2}').format(self.tun.client.str, self.iface_name, e)) ipr.close() raise e try: 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)) ipr.close() raise e try: ipr.route('add', dst = 'default', gateway = self.tun.server.str, 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} with ' 'gateway {1}: {2}').format(self.iface_name, self.tun.server.str, e)) ipr.close() raise e for assignment in self.tun.assignments: # The SLAAC prefixes. for b in assignment.iface_blocks: # Try to remove first in case it's already assigned. try: ipr.addr('del', index = assignment.iface_idx, address = str(b.ip), mask = b.prefixlen, family = socket.AF_INET6) logger.debug('Removed {0} with prefix {1} from {2}.'.format(str(b), b.prefixlen, assignment.iface)) except Exception as e: pass try: ipr.addr('add', index = assignment.iface_idx, address = str(b.ip), mask = b.prefixlen, family = socket.AF_INET6) logger.debug('Added {0} with prefix {1} to {2}.'.format(str(b.ip), b.prefixlen, assignment.iface)) except Exception as e: logger.error(('Could not add address block {0} with prefix {1} on {2}: ' '{3}').format(str(b.ip), b.prefixlen, assignment.iface, e)) ipr.close() raise e ipr.close() if self.tun.ra: self.tun.ra.conf.write() self.tun.ra.svc.restart() 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 = 'default', # gateway = self.def_rt_ip, 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() self.tun.ra.svc.stop() return(None) def update(self): if not self.my_ip: self._get_my_ip() if not self.needs_update: return(None) auth_handler = requests.auth.HTTPBasicAuth(self.tun.creds.user, self.tun.update_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.str}, 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)