diff --git a/utils/he_ipv6/config.py b/utils/he_ipv6/config.py index e13c0a3..6ebd174 100644 --- a/utils/he_ipv6/config.py +++ b/utils/he_ipv6/config.py @@ -16,9 +16,9 @@ def xml2bool(xml_str): if xml_str is None: return(None) xml_str = xml_str.lower()[0] - if xml_str == ('t', '1'): + if xml_str in ('t', '1'): return(True) - elif xml_str == ('f', '0'): + elif xml_str in ('f', '0'): return(False) else: raise ValueError('Not a boolean value') @@ -67,140 +67,6 @@ class IP6(IP): self.alloc_block = netaddr.SubnetSplitter(self.net_net) -class Assignment(object): - def __init__(self, assign_xml, radvd = False, dns = False): - self.xml = assign_xml - self.do_radvd = radvd - self.radvd_dns = dns - self.iface = None - self.alloc = None # This must be set externally to a mapped Allocation instance - self.alloc_name = None - self.prefix = None - self.iface_ip = None - self.alloc_block = None - self.parse() - - def _alloc(self): - self.alloc_name = self.xml.attrib['alloc'].strip() - return(None) - - def _iface(self): - self.iface = self.xml.attrib['iface'].strip() - return(None) - - def _prefix(self): - self.prefix = int(self.xml.attrib.get('prefix', 64).strip()) - return(None) - - def parse(self): - self._iface() - self._alloc() - self._prefix() - return(None) - - def parse_alloc(self): - self.iface_ip = IP6(str(next(self.alloc.ip.net.hosts())), 128) - self.alloc_block = self.alloc.ip.alloc_block - return(None) - - -class Allocation(object): - def __init__(self, alloc_xml): - self.xml = alloc_xml - self.id = None - self.prefix = None - self.ip = None - self.iface = None - self.iface_idx = None - self.parse() - - def _id(self): - self.id = self.xml.attrib['id'].strip() - return(None) - - def _iface(self): - _iface_txt = self.xml.attrib['iface'] - self.iface = _iface_txt.strip() - ipr = IPRoute() - self.iface_idx = ipr.link_lookup(ifname = self.iface)[0] - ipr.close() - return(None) - - def _ip(self): - _ip_txt = self.xml.text.strip() - _prefix_txt = self.xml.attrib['prefix'].strip() - self.ip = IP6(_ip_txt, _prefix_txt) - self.prefix = self.ip.prefix - return(None) - - def parse(self): - self._id() - self._iface() - self._ip() - return(None) - - -class Tunnel(object): - def __init__(self, tun_xml): - self.xml = tun_xml - self.id = None - self.client = None - self.server = None - self.creds = None # This should be handled externally and map to a Cred obj - self.creds_id = None - self.allocations = {} - self.assignments = [] - self.parse() - - def _allocations(self): - _allocs_xml = self.xml.find('allocations') - for _allocation_xml in _allocs_xml.findall('alloc'): - alloc = Allocation(_allocation_xml) - self.allocations[alloc.id] = alloc - return(None) - - def _assignments(self): - _assigns_xml = self.xml.find('assignments') - radvd = xml2bool(_assigns_xml.attrib.get('radvd', 'false')) - radvd_dns = xml2bool(_assigns_xml.attrib.get('radvdDns', 'false')) - for _assign_xml in _assigns_xml.findall('assign'): - assign = Assignment(_assign_xml, radvd = radvd, dns = radvd_dns) - assign.alloc = self.allocations[assign.alloc_name] - assign.parse_alloc() - self.assignments.append(assign) - return(None) - - def _client(self): - _client_xml = self.xml.find('client') - _ip_txt = _client_xml.text.strip() - _prefix_txt = _client_xml.attrib['prefix'].strip() - self.client = IP6(_ip_txt, _prefix_txt) - return(None) - - def _creds(self): - self.creds_id = self.xml.attrib['creds'].strip() - return(None) - - def _id(self): - self.id = int(self.xml.attrib['id'].strip()) - return(None) - - def _server(self): - _server_xml = self.xml.find('server') - _ip_text = _server_xml.text.strip() - self.server = IP4(_ip_text, 32) - return(None) - - def parse(self): - self._id() - self._creds() - self._client() - self._server() - self._allocations() - self._assignments() - return(None) - - class Credential(object): def __init__(self, cred_xml): self.xml = cred_xml @@ -243,6 +109,148 @@ class Credential(object): return(None) +class Assignment(object): + def __init__(self, assign_xml, radvd = False, dns = False): + self.xml = assign_xml + self.do_radvd = radvd + self.radvd_dns = dns + self.iface = None + self.iface_idx = None + self.iface_addrs = [] + self.iface_blocks = [] + self.alloc = None # This must be set externally to a mapped Allocation instance + self.alloc_name = None + self.prefix = None + self.alloc_block = None + self.parse() + + def _alloc(self): + self.alloc_name = self.xml.attrib['alloc'].strip() + return(None) + + def _iface(self): + _iface_txt = self.xml.attrib['iface'].strip() + self.iface = _iface_txt.strip() + ipr = IPRoute() + self.iface_idx = ipr.link_lookup(ifname = self.iface)[0] + ipr.close() + return(None) + + def _prefix(self): + self.prefix = int(self.xml.attrib.get('prefix', 64).strip()) + return(None) + + def parse(self): + self._iface() + self._alloc() + self._prefix() + return(None) + + def parse_alloc(self): + self.alloc_block = self.alloc.ip.alloc_block + self.iface_blocks = self.alloc_block.extract_subnet(self.prefix, count = 1) + for i in self.iface_blocks: + self.iface_addrs.append(IP6(str(next(i.iter_hosts())), 128)) + return(None) + + +class Allocation(object): + def __init__(self, alloc_xml): + self.xml = alloc_xml + self.id = None + self.prefix = None + self.ip = None + self.iface = None + self.iface_idx = None + self.parse() + + def _id(self): + self.id = self.xml.attrib['id'].strip() + return(None) + + def _ip(self): + _ip_txt = self.xml.text.strip() + _prefix_txt = self.xml.attrib['prefix'].strip() + self.ip = IP6(_ip_txt, _prefix_txt) + self.prefix = self.ip.prefix + return(None) + + def parse(self): + self._id() + self._ip() + return(None) + + +class Tunnel(object): + def __init__(self, tun_xml): + self.xml = tun_xml + self.id = None + self.client = None + self.server = None + self.creds = None # This should be handled externally and map to a Cred obj + self.creds_id = None + self.radvd = None + self.enable_radvd = None + self.radvd_dns = None + self.allocations = {} # This is a dict of {}[alloc.id] = Allocation obj + self.assignments = [] # This is a list of Assignment objs + self.parse() + + def _allocations(self): + _allocs_xml = self.xml.find('allocations') + for _allocation_xml in _allocs_xml.findall('alloc'): + alloc = Allocation(_allocation_xml) + self.allocations[alloc.id] = alloc + return(None) + + def _assignments(self): + _assigns_xml = self.xml.find('assignments') + self.enable_radvd = xml2bool(_assigns_xml.attrib.get('radvd', 'false')) + self.radvd_dns = xml2bool(_assigns_xml.attrib.get('radvdDns', 'false')) + for _assign_xml in _assigns_xml.findall('assign'): + assign = Assignment(_assign_xml, radvd = self.enable_radvd, dns = self.radvd_dns) + assign.alloc = self.allocations[assign.alloc_name] + assign.parse_alloc() + self.assignments.append(assign) + return(None) + + def _client(self): + _client_xml = self.xml.find('client') + _ip_txt = _client_xml.text.strip() + _prefix_txt = _client_xml.attrib['prefix'].strip() + self.client = IP6(_ip_txt, _prefix_txt) + return(None) + + def _creds(self): + self.creds_id = self.xml.attrib['creds'].strip() + return(None) + + def _id(self): + self.id = int(self.xml.attrib['id'].strip()) + return(None) + + def _radvd(self): + self.radvd = radvd.RADVD() + self.radvd.conf.generate(self.assignments) + return(None) + + def _server(self): + _server_xml = self.xml.find('server') + _ip_text = _server_xml.text.strip() + self.server = IP4(_ip_text, 32) + return(None) + + def parse(self): + self._id() + self._creds() + self._client() + self._server() + self._allocations() + self._assignments() + self._radvd() + return(None) + + class Config(object): default_xsd = 'http://schema.xml.r00t2.io/projects/he_ipv6.xsd' diff --git a/utils/he_ipv6/radvd.py b/utils/he_ipv6/radvd.py index 052d204..bbbcea2 100644 --- a/utils/he_ipv6/radvd.py +++ b/utils/he_ipv6/radvd.py @@ -14,6 +14,8 @@ class RADVDSvc(object): self.name = self.svc_name self.is_systemd = False self._get_manager() + self.cmd_tpl = None + self.has_pkill = False def _get_manager(self): chkpaths = ('/run/systemd/system', @@ -23,24 +25,21 @@ class RADVDSvc(object): if os.path.exists(_): self.is_systemd = True break - return(None) - - def restart(self): if self.is_systemd: - cmd = ['systemd', 'restart', self.name] + self.cmd_tpl = 'systemctl {op} {name}' else: # Systemd haters, if you don't understand the benefits of unified service management across all linux # distros, you've obviously never done wide-platform management or scripting. # Let this else block be a learning experience for you. - cmd = None - has_pkill = False + self.cmd_tpl = None + self.has_pkill = False for p in os.environ.get('PATH', '/usr/bin').split(':'): fpath = os.path.abspath(os.path.expanduser(p)) bins = os.listdir(fpath) if 'pkill' in bins: - has_pkill = True + self.has_pkill = True if 'service' in bins: # CentOS/RHEL pre-7.x - cmd = ['service', self.name, 'restart'] + self.cmd_tpl = 'service {name} {op}' break elif 'initctl' in bins: # older Ubuntu and other Upstart distros cmd = ['initctl', 'restart', self.name] @@ -49,20 +48,43 @@ class RADVDSvc(object): cmd = ['rc-service', self.name, 'restart'] break # That wasn't even all of them. - if not cmd and has_pkill: # last-ditch effort. - cmd = ['pkill', '-HUP', self.name] - if not cmd: + # This doesn't make sense since we template the command now. + # if not self.cmd_tpl and self.has_pkill: # last-ditch effort. + # cmd = ['pkill', '-HUP', self.name] + if not self.cmd_tpl: logger.error('Could not find which service manager this system is using.') raise RuntimeError('Could not determine service manager') - cmd_exec = subprocess.run(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE) - if cmd_exec.returncode != 0: - logger.warning('Could not successfully restart {0}; returned status {1}'.format(self.name, - cmd_exec.returncode)) - for i in ('stdout', 'stderr'): - s = getattr(cmd_exec, i).decode('utf-8') - if s.strip() != '': - logger.warning('{0}: {1}'.format(i.upper(), s)) - warnings.warn('Service did not restart successfully') + return(None) + + def restart(self): + self.stop() + self.start() + return(None) + + def start(self): + cmd = self.cmd_tpl.format(op = 'start', name = self.name).split() + cmd_exec = subprocess.run(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE) + if cmd_exec.returncode != 0: + logger.warning('Could not successfully start {0}; returned status {1}'.format(self.name, + cmd_exec.returncode)) + for i in ('stdout', 'stderr'): + s = getattr(cmd_exec, i).decode('utf-8') + if s.strip() != '': + logger.warning('{0}: {1}'.format(i.upper(), s)) + warnings.warn('Service did not start successfully') + return(None) + + def stop(self): + cmd = self.cmd_tpl.format(op = 'stop', name = self.name).split() + cmd_exec = subprocess.run(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE) + if cmd_exec.returncode != 0: + logger.warning('Could not successfully stop {0}; returned status {1}'.format(self.name, + cmd_exec.returncode)) + for i in ('stdout', 'stderr'): + s = getattr(cmd_exec, i).decode('utf-8') + if s.strip() != '': + logger.warning('{0}: {1}'.format(i.upper(), s)) + warnings.warn('Service did not stop successfully') return(None) @@ -76,20 +98,20 @@ class RADVDConf(object): '\tMaxRtrAdvInterval 600;\n' '\tAdvDefaultLifetime 9000;\n' '{prefix}' - 'route ::/0 {{\n' - '\t\tAdvRouteLifetime infinity;' - '}};\n' + '\troute ::/0 {{\n' + '\t\tAdvRouteLifetime infinity;\n' + '\t}};\n' '{rdnss}' '}};\n\n') tpl_prefix = ('\tprefix {subnet} {{\n' - '\t\tAdvOnLink on;' - '\t\tAdvAutonomous on;' + '\t\tAdvOnLink on;\n' + '\t\tAdvAutonomous on;\n' '\t\tAdvRouterAddr off;\n' - '}};\n') + '\t}};\n') tpl_rdnss = ('\tRDNSS {client_ip} {{\n' '\t\tAdvRDNSSOpen on;\n' '\t\tAdvRDNSSPreference 2;\n' - '}};\n') + '\t}};\n') def __init__(self, cfg = None): if not cfg: @@ -101,12 +123,21 @@ class RADVDConf(object): def generate(self, assign_objs): self.cfgStr = '' for assign_obj in assign_objs: - prefix = self.tpl_prefix.format(subnet = str(assign_obj.net)) - if assign_obj.dns: - dns = self.tpl_rdnss.format(client_ip = assign_obj.iface_ip.str) - else: - dns = '' - self.cfgStr += self.tpl.format(prefix = prefix, rdnss = dns) + if not assign_obj.do_radvd: + continue + for b in assign_obj.iface_blocks: + prefix = self.tpl_prefix.format(subnet = str(b)) + if assign_obj.radvd_dns: + dns = self.tpl_rdnss.format(client_ip = str(next(b.iter_hosts()))) + else: + dns = '' + self.cfgStr += self.tpl.format(prefix = prefix, rdnss = dns, iface = assign_obj.iface) + return(None) + + def write(self): + with open(self.cfg, 'w') as fh: + fh.write(self.cfgStr) + return(None) class RADVD(object): diff --git a/utils/he_ipv6/tunnelbroker.py b/utils/he_ipv6/tunnelbroker.py index 8cdca55..dc36361 100644 --- a/utils/he_ipv6/tunnelbroker.py +++ b/utils/he_ipv6/tunnelbroker.py @@ -14,7 +14,6 @@ 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_ip = '::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)) @@ -109,7 +108,7 @@ class TunnelBroker(object): try: ipr.route('add', dst = 'default', - # gateway = self.def_rt_ip, + 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)) @@ -117,28 +116,37 @@ class TunnelBroker(object): logger.error(('Could not add default IPv6 route on link {0}: {1}').format(self.iface_name, e)) ipr.close() raise e - for alloc in self.tun.allocations: - try: - ipr.addr('add', - index = alloc.iface_idx, - address = alloc.ip.str, - mask = alloc.ip.prefix, - family = socket.AF_INET6) - except Exception as e: - logger.error(('Could not add address {0} on link {1}: ' - '{2}').format(str(alloc.ip.str), alloc.iface_idx, e)) - ipr.close() - raise e - # Is this necessary? - # try: - # ipr.route('add', - # dst = '::/96', - # gateway = '::', - # oif = self.iface_idx, - # family = socket.AF_INET6) - # except Exception as e: - # logger.error(('Could not add ::/96 on link {0}: {1}'.format(self.iface_name, e))) + for assignment in self.tun.assignments: + for a in assignment.iface_addrs: + # The interface-specific ":1" addrs. + try: + ipr.addr('add', + index = assignment.iface_idx, + address = a.ip.str, + mask = a.ip.prefix, + family = socket.AF_INET6) + except Exception as e: + logger.error(('Could not add address {0} on link {1}: ' + '{2}').format(str(a.ip.str), assignment.iface_idx, e)) + ipr.close() + raise e + # The SLAAC prefixes. + for b in assignment.iface_blocks: + try: + ipr.addr('add', + index = assignment.iface_idx, + address = str(b.ip), + mask = b.prefixlen, + family = socket.AF_INET6) + except Exception as e: + logger.error(('Could not add address block {0} on link {1}: ' + '{2}').format(str(b), assignment.iface_idx, e)) + ipr.close() + raise e ipr.close() + if self.tun.enable_radvd: + self.tun.radvd.conf.write() + self.tun.radvd.svc.restart() return(None) def stop(self): @@ -172,6 +180,7 @@ class TunnelBroker(object): ipr.close() raise e ipr.close() + self.tun.radvd.svc.stop() return(None) def update(self, oneshot = False):