diff --git a/utils/he_ipv6/__init__.py b/utils/he_ipv6/__init__.py index ca2774b..6d60a0c 100644 --- a/utils/he_ipv6/__init__.py +++ b/utils/he_ipv6/__init__.py @@ -1,5 +1,5 @@ from . import args -from . import radvd +from . import ra from . import tunnel from . import config from . import logger diff --git a/utils/he_ipv6/example.tunnelbroker.xml b/utils/he_ipv6/example.tunnelbroker.xml index 67cb0db..4264e1d 100644 --- a/utils/he_ipv6/example.tunnelbroker.xml +++ b/utils/he_ipv6/example.tunnelbroker.xml @@ -41,12 +41,10 @@ - + - + + + + true + true + + - + + + diff --git a/utils/he_ipv6/ra.py b/utils/he_ipv6/ra.py new file mode 100644 index 0000000..1ddc623 --- /dev/null +++ b/utils/he_ipv6/ra.py @@ -0,0 +1,164 @@ +import logging +import os +import subprocess +import warnings +## +import jinja2 + + +logger = logging.getLogger() +def_tpl_dir = os.path.join(os.path.dirname(os.path.abspath(os.path.expanduser(__file__))), 'tpl') + + +class RA(object): + def __init__(self, conf = None, tpl_name = None, tpl_dir = None, *args, **kwargs): + self.conf = RAConf(conf = conf, tpl_name = tpl_name, tpl_dir = tpl_dir, *args, **kwargs) + self.svc = RASvc() + + +class RAConf(object): + def_tpl_name = None + def_conf = None + cfgstr = None + + def __init__(self, conf = None, tpl_name = None, tpl_dir = None, *args, **kwargs): + for k in ('name', 'dir'): + n = 'tpl_{0}'.format(k) + d = 'def_tpl_{0}'.format(k) + v = locals()[k] + if not v: + setattr(self, n, getattr(self, d)) + else: + setattr(self, n, v) + if not conf: + self.conf = self.def_conf + else: + self.conf = os.path.abspath(os.path.expanduser(conf)) + + def ext_init(self): + self.tpl_dir = os.path.abspath(os.path.expanduser(self.tpl_dir)) + self.loader = jinja2.FileSystemLoader(self.tpl_dir) + self.tpl_env = jinja2.Environment(loader = self.loader) + self.tpl = self.tpl_env.get_template(self.tpl_name) + return(None) + + def generate(self, assignments): + ns = {} + for a in assignments: + if len(a.iface_addrs) > 3: + ns_addrs = a.iface_addrs[:3] + else: + ns_addrs = a.iface_addrs + ns[a.iface] = ns_addrs + self.cfgstr = self.tpl.render(assignments = assignments, nameservers = ns) + + def write(self): + if not self.cfgstr: + raise RuntimeError('Must run .generate() first') + os.makedirs(os.path.dirname(self.conf), exist_ok = True, mode = 0o0700) + with open(self.conf, 'w') as fh: + fh.write(self.cfgstr) + + +class RASvc(object): + name = None + is_systemd = False + cmd_tpl = None + has_pkill = False + start_cmd = None + stop_cmd = None + restart_cmd = None + + def __init__(self): + self._get_manager() + + def _exec(self, cmd): + cmd_exec = subprocess.run(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE) + if cmd_exec.returncode != 0: + logger.warning('Could not execute {0}; returned status {1}'.format(' '.join(cmd), + 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)) + return(None) + + def _get_manager(self): + chkpaths = ('/run/systemd/system', + '/dev/.run/systemd', + '/dev/.systemd') + for _ in chkpaths: + if os.path.exists(_): + self.is_systemd = True + break + if self.is_systemd: + 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. + 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: + self.has_pkill = True + if 'service' in bins: # CentOS/RHEL pre-7.x + self.cmd_tpl = 'service {name} {op}' + break + elif 'initctl' in bins: # older Ubuntu and other Upstart distros + self.cmd_tpl = 'initctl {op} {name}' + break + elif 'rc-service' in bins: # OpenRC + self.cmd_tpl = 'rc-service {name} {op}' + break + # That wasn't even all of them. + if not self.cmd_tpl and not self.has_pkill: + logger.error('Could not find which service manager this system is using.') + raise RuntimeError('Could not determine service manager') + elif self.has_pkill: # Last-ditch effort. + self.start_cmd = [self.name] + self.stop_cmd = ['pkill', self.name] + self.restart_cmd = ['pkill', '-HUP', self.name] + else: + for k in ('start', 'stop', 'restart'): + setattr(self, + '{0}_cmd'.format(k), + self.cmd_tpl.format(name = self.name, op = k).split()) + return(None) + + def restart(self): + cmd = self.restart_cmd + self._exec(cmd) + return(None) + + def start(self): + cmd = self.start_cmd + self._exec(cmd) + return(None) + + def stop(self): + cmd = self.stop_cmd + self._exec(cmd) + return(None) + + +class RADVD(RA): + name = 'radvd' + def_conf = '/etc/radvd.conf' + def_tpl_name = 'radvd.conf.j2' + + def __init__(self, conf = None, tpl_name = None, tpl_dir = None): + super().__init__(conf = conf, tpl_name = tpl_name, tpl_dir = tpl_dir) + self.conf.ext_init() + + +class DNSMasq(RA): + name = 'dnsmasq' + def_conf = '/etc/dnsmasq.d/ra.conf' + def_tpl_name = 'dnsmasq.include.j2' + + def __init__(self, conf = None, tpl_name = None, tpl_dir = None): + super().__init__(conf = conf, tpl_name = tpl_name, tpl_dir = tpl_dir) + self.conf.ext_init() diff --git a/utils/he_ipv6/radvd.py b/utils/he_ipv6/radvd.py deleted file mode 100644 index fcd26dd..0000000 --- a/utils/he_ipv6/radvd.py +++ /dev/null @@ -1,143 +0,0 @@ -import logging -import os -import subprocess -import warnings - - -logger = logging.getLogger() - - -class RADVDSvc(object): - svc_name = 'radvd' - - def __init__(self): - self.name = self.svc_name - self.is_systemd = False - self.cmd_tpl = None - self.has_pkill = False - self._get_manager() - - def _get_manager(self): - chkpaths = ('/run/systemd/system', - '/dev/.run/systemd', - '/dev/.systemd') - for _ in chkpaths: - if os.path.exists(_): - self.is_systemd = True - break - if self.is_systemd: - 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. - 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: - self.has_pkill = True - if 'service' in bins: # CentOS/RHEL pre-7.x - self.cmd_tpl = 'service {name} {op}' - break - elif 'initctl' in bins: # older Ubuntu and other Upstart distros - cmd = ['initctl', 'restart', self.name] - break - elif 'rc-service' in bins: # OpenRC - cmd = ['rc-service', self.name, 'restart'] - break - # That wasn't even all of them. - # 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') - 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) - - -class RADVDConf(object): - path = '/etc/radvd.conf' - tpl = ('interface {iface} {{\n' - '\tAdvSendAdvert on;\n' - # '\tAdvLinkMTU 1280;\n' # Is it 1480 or 1280? Arch wiki says 1480, but everything else (older) says 1280. - '\tAdvLinkMTU 1480;\n' - '\tMinRtrAdvInterval 60;\n' - '\tMaxRtrAdvInterval 600;\n' - '\tAdvDefaultLifetime 9000;\n' - '{prefix}' - '\troute ::/0 {{\n' - '\t\tAdvRouteLifetime infinity;\n' - '\t}};\n' - '{rdnss}' - '}};\n\n') - tpl_prefix = ('\tprefix {subnet} {{\n' - '\t\tAdvOnLink on;\n' - '\t\tAdvAutonomous on;\n' - '\t\tAdvRouterAddr off;\n' - '\t}};\n') - tpl_rdnss = ('\tRDNSS {client_ip} {{}};\n') - - def __init__(self, cfg = None): - if not cfg: - self.cfg = self.path - else: - self.cfg = os.path.abspath(os.path.expanduser(cfg)) - self.cfgStr = None - - def generate(self, assign_objs): - self.cfgStr = '' - for assign_obj in assign_objs: - 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): - def __init__(self): - self.svc = RADVDSvc() - self.conf = RADVDConf(cfg = '/etc/radvd.conf') diff --git a/utils/he_ipv6/tpl/dnsmasq.include.j2 b/utils/he_ipv6/tpl/dnsmasq.include.j2 new file mode 100644 index 0000000..09b100b --- /dev/null +++ b/utils/he_ipv6/tpl/dnsmasq.include.j2 @@ -0,0 +1,2 @@ +# This file should be *included* in your dnsmasq configuration. + diff --git a/utils/he_ipv6/tpl/radvd.conf.j2 b/utils/he_ipv6/tpl/radvd.conf.j2 new file mode 100644 index 0000000..27a210f --- /dev/null +++ b/utils/he_ipv6/tpl/radvd.conf.j2 @@ -0,0 +1,30 @@ +# Generated by he_ipv6 +{% for assign in assignments %} + {% for assignment in assign.iface_blocks %} +interface {{ assignment.iface }} { + AdvSendAdvert on; + # Is it 1480 or 1280? Arch wiki says 1480, but everything else (older) says 1280. + # AdvLinkMTU 1280; + AdvLinkMTU 1480; + MinRtrAdvInterval 60; + MaxRtrAdvInterval 600; + AdvDefaultLifetime 9000; + + {%- for block in assignment.iface_blocks %} + prefix {{ block|string }} { + AdvOnLink on; + {%- if block.prefixlen == 64 %} + AdvAutonomous on; + {%- endif %} + AdvRouterAddr off; + }; + {%- endfor %} + + {%- if ra.dns is true %} + RDNSS {{ nameservers[assignment.iface]|join(' ') }} { + }; + {%- endif %} +}; + + {%- endfor %} +{%- endfor %} diff --git a/utils/he_ipv6/tunnel.py b/utils/he_ipv6/tunnel.py index 6a9d8c1..b0009d2 100644 --- a/utils/he_ipv6/tunnel.py +++ b/utils/he_ipv6/tunnel.py @@ -4,7 +4,7 @@ import netaddr from pyroute2 import IPRoute ## from . import utils -from . import radvd +from . import ra class IP(object): @@ -51,10 +51,10 @@ class IP6(IP): class Assignment(object): - def __init__(self, assign_xml, radvd = False, dns = False): + def __init__(self, assign_xml, ra = False, dns = False, ra_provider = 'dnsmasq'): self.xml = assign_xml - self.do_radvd = radvd - self.radvd_dns = dns + self.ra = ra + self.dns = dns self.iface = None self.iface_idx = None self.iface_addrs = [] @@ -114,9 +114,10 @@ class Tunnel(object): self.client = None self.server = None self.endpoint = None - self.radvd = None - self.enable_radvd = None - self.radvd_dns = None + self.ra = False + self.ra_provider = None + self.ra_dns = False + self.ra_dhcp = False self.allocations = {} # This is a dict of {}[alloc.id] = Allocation obj self.assignments = [] # This is a list of Assignment objs self.parse() @@ -127,10 +128,11 @@ class Tunnel(object): def _assignments(self): _assigns_xml = self.xml.find('assignments') - self.enable_radvd = utils.xml2bool(_assigns_xml.attrib.get('radvd', 'false')) - self.radvd_dns = utils.xml2bool(_assigns_xml.attrib.get('radvdDns', 'false')) + + self.enable_ra = utils.xml2bool(_assigns_xml.attrib.get('radvd', 'false')) + self.ra_dns = utils.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 = Assignment(_assign_xml, ra = self.enable_ra, dns = self.ra_dns) assign.alloc = self.allocations[assign.alloc_id] assign.parse_alloc() self.assignments.append(assign) @@ -153,7 +155,7 @@ class Tunnel(object): return(None) def _radvd(self): - self.radvd = radvd.RADVD() + self.radvd.conf.generate(self.assignments) return(None)