diff --git a/utils/he_ipv6/__init__.py b/utils/he_ipv6/__init__.py new file mode 100644 index 0000000..eab7d37 --- /dev/null +++ b/utils/he_ipv6/__init__.py @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..08d38c5 --- /dev/null +++ b/utils/he_ipv6/args.py @@ -0,0 +1,32 @@ +import argparse + + +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) diff --git a/utils/he_ipv6/config.py b/utils/he_ipv6/config.py index 14d55b7..f021bb8 100644 --- a/utils/he_ipv6/config.py +++ b/utils/he_ipv6/config.py @@ -1,3 +1,4 @@ +import collections import copy import ipaddress import os @@ -6,6 +7,7 @@ import re import netaddr import requests from lxml import etree +from pyroute2 import IPRoute class IP(object): @@ -56,11 +58,15 @@ class Allocation(object): self.prefix = None self.ip = None self.iface = None + self.iface_idx = None self.parse() 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): @@ -173,7 +179,7 @@ class Config(object): self.xsd = None self.defaults_parser = None self.obj = None - self.tunnels = {} + self.tunnels = collections.OrderedDict() self.creds = {} self.parse() diff --git a/utils/he_ipv6/logger.py b/utils/he_ipv6/logger.py new file mode 100644 index 0000000..203bc4d --- /dev/null +++ b/utils/he_ipv6/logger.py @@ -0,0 +1,49 @@ +import logging +import logging.handlers +import os +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 + + +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.') diff --git a/utils/he_ipv6/main.py b/utils/he_ipv6/main.py new file mode 100755 index 0000000..1f3be5f --- /dev/null +++ b/utils/he_ipv6/main.py @@ -0,0 +1,24 @@ +import logging +## +from . import args +from . import tunnelbroker + + +logger = logging.getLogger() + + +def main(): + _args = args.parseArgs().parse_args() + logger.debug('Invoked with args: {0}'.format(vars(_args))) + tb = tunnelbroker.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/ref b/utils/he_ipv6/ref new file mode 100644 index 0000000..fa92ac4 --- /dev/null +++ b/utils/he_ipv6/ref @@ -0,0 +1,4 @@ +# 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 diff --git a/utils/he_ipv6/tunnelbroker.py b/utils/he_ipv6/tunnelbroker.py new file mode 100644 index 0000000..4e80231 --- /dev/null +++ b/utils/he_ipv6/tunnelbroker.py @@ -0,0 +1,48 @@ +import logging +import os +import socket +logger = logging.getLogger() +## +import requests +from pyroute2 import IPRoute +## +from . import config + + +class TunnelBroker(object): + url_ip = 'https://ipv4.clientinfo.square-r00t.net/' + params_ip = {'raw': '1'} + url_api = 'https://ipv4.tunnelbroker.net/nic/update' + + 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.cfg = self._conf.tunnels[int(tun_id)] + else: + tun_id = list(self._conf.tunnels.keys())[0] + self.cfg = self._conf.tunnels[tun_id] + self.wan = wan_ip + self.update = update + self.my_ip = None + + def _get_my_ip(self): + 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 = config.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 = config.IP4(_defrt[0]['attrs']['RTA_PREFSRC'], 32) + logger.debug('Set my_ip to {0}.'.format(self.my_ip.str)) + return(None)