Compare commits

...

66 Commits

Author SHA1 Message Date
brent s. d13ecf8d20
checking in some work 2020-06-18 23:21:07 -04:00
brent s. cd422fc7d6
let's see what i just fucked up. 2020-05-18 08:35:37 -04:00
brent s. e589e00100
heh erm. whoop 2020-05-18 08:15:40 -04:00
brent s. 01acb18f5f
okay, i think i fixed the range allocation issue. 2020-05-18 08:13:39 -04:00
brent s. 50fd503ce6
AHA. it was choking out on :: for /48s! 2020-05-18 07:21:13 -04:00
brent s. 753d5007d8
more logging. 2020-05-18 07:18:01 -04:00
brent s. 4942e7d662
more logging. is it even being *set*? 2020-05-18 07:11:59 -04:00
brent s. befdc9e99b
it doesn't seem to be recognizing additional DHCPv6 ranges in assignments? 2020-05-18 07:08:34 -04:00
brent s. 361303e87a
wheretf did i even see "sa-names"? it's not in the man page or sample conf. 2020-05-18 07:00:09 -04:00
brent s. 39c8cdc171
dang it 2020-05-18 06:56:19 -04:00
brent s. 51184c299d
just need to sort out the dnsmasq generation 2020-05-18 06:55:18 -04:00
brent s. 1b82e67c93
lol still dum 2020-05-18 06:47:33 -04:00
brent s. 8ad2fbff22
lol broke logging 2020-05-18 06:32:35 -04:00
brent s. 3d238dda22
hrm 2020-05-18 06:31:05 -04:00
brent s. d1a9972786
D'OH. dumb mistake 2020-05-18 06:21:48 -04:00
brent s. 3ec21c4fa9
why won't it set RASvc.name? 2020-05-18 06:11:14 -04:00
brent s. a992fac902
and this 2020-05-18 06:06:50 -04:00
brent s. bdcbc09dc7
fix this 2020-05-18 05:53:42 -04:00
brent s. 8cb29762a7
and this too 2020-05-18 05:48:40 -04:00
brent s. ca594b09dd
forgot to do this 2020-05-18 05:46:50 -04:00
brent s. afd839a195
d'oh 2020-05-18 05:44:31 -04:00
brent s. 92f857d967
ONE day i'll get initialization of subclasses/superclasses right. 2020-05-18 05:42:44 -04:00
brent s. bc12a6ad84
hrm 2020-05-18 05:41:00 -04:00
brent s. 0868b18de8
oops 2020-05-18 05:35:05 -04:00
brent s. 7b48e6813c
bites you in the ass. every time. 2020-05-18 05:33:57 -04:00
brent s. 87dfe6a543
oop 2020-05-18 05:27:15 -04:00
brent s. 1655dd1e89
bit more logging 2020-05-18 05:24:43 -04:00
brent s. 80765e58ed
okay. let's give this a shot. 2020-05-18 04:59:00 -04:00
brent s. 4df9287abd
gonna remove some of these. 2020-05-18 02:26:49 -04:00
brent s. 5f2883a698
okay. so the config's cleaned up, and we now create a sparse example config file. 2020-05-16 03:48:02 -04:00
brent s. a0d5071a8d
i'm... going to re-do this. 2020-05-16 01:47:06 -04:00
brent s 363cdc712e
holy shit, more restructuring 2020-05-15 18:01:03 -04:00
brent s. 429cf7b155
deprecated flags 2020-05-15 04:26:22 -04:00
brent s. 85cdc0c52a
whoops, bad ordering 2020-05-15 03:33:06 -04:00
brent s. b5b47e92fd
forgot these don't have .str attrs 2020-05-15 03:23:53 -04:00
brent s. 32c78201e8
missed some 2020-05-15 03:21:12 -04:00
brent s. 51dadf421e
ditto 2020-05-15 03:20:04 -04:00
brent s. c877868c33
try to remove the address first 2020-05-15 03:17:53 -04:00
brent s. 881a8c9317
...better logging 2020-05-15 03:15:50 -04:00
brent s. 1d27ee0556
even moar logging 2020-05-15 03:14:41 -04:00
brent s. 754fa3eb25
let's clean this up 2020-05-15 03:07:07 -04:00
brent s. 1b090c76ce
fixing a small issue with the client ip 2020-05-15 03:02:39 -04:00
brent s. 6248422962
i think this should be better 2020-05-15 02:49:19 -04:00
brent s. 375a8c8427
MOAR logging 2020-05-15 02:47:08 -04:00
brent s. 92fdee435a
logging? wtf? 2020-05-15 02:15:45 -04:00
brent s. c45754b1a3
ensure we update if no cached IPs 2020-05-15 01:53:37 -04:00
brent s. a8475d9001
erm. use the .str attr. otherwise we get the class name. duh. 2020-05-15 01:51:34 -04:00
brent s. 58e495bf41
whoops. SO CLOSEEEE 2020-05-15 01:50:45 -04:00
brent s. 9246afa9f7
SO close. 2020-05-15 01:49:23 -04:00
brent s. f37d26572a
first-run cache issue 2020-05-15 01:48:26 -04:00
brent s. 05ef5b078c
getting there 2020-05-15 01:45:52 -04:00
brent s. 58cbbb06cd
whoops! forgot to reference the text. 2020-05-15 01:42:43 -04:00
brent s. cf51e96852
so it turns out it returns the same structure, just with a single <tunnels> child. i can probably just use the general HEConf then and wrap it? 2020-05-15 01:40:42 -04:00
brent s. b00351f762
oops 2020-05-15 01:35:23 -04:00
brent s. 66561c51d8
i cannot BELIEVE i did that. 2020-05-15 00:41:15 -04:00
brent s. 742a0b55d5
i...think it's ready to test. 2020-05-14 23:46:03 -04:00
brent s efb53be81b
more config stuff 2020-05-14 21:21:11 -04:00
brent s 315af935ac
restructuring and removing HEConfig 2020-05-14 17:12:42 -04:00
brent s. 676aa8d5b6
this is currently super broken but i'm getting there. 2020-05-14 14:23:14 -04:00
brent s. 8a5d484883
oops 2020-05-14 13:50:50 -04:00
brent s. c8c4957120
okay, let's see what i broke 2020-05-14 13:21:40 -04:00
brent s. c23c803a20
update some config stuff... not done 2020-05-14 04:05:32 -04:00
brent s. fb89feb046
i'm an idiot. i spent like 15 minutes debugging this. 2020-05-14 03:46:55 -04:00
brent s. 9a8bae0ba8
OSError: illegal IP address string passed to inet_pton 2020-05-14 01:19:10 -04:00
brent s. cb1a877ddc
i think i got it 2020-05-13 21:10:09 -04:00
brent s. fc8eda8198
some updates to how i handle address allocation for he_ipv6 2020-05-13 13:31:16 -04:00
15 changed files with 1037 additions and 248 deletions

3
utils/he_ipv6/TODO Normal file
View File

@ -0,0 +1,3 @@
DHCPv6:
* NTP server in <ra>? (dnsmasq: option6:ntpserver,...)
* bootfile-(url|param) in <ra>? (dnsmasq: option6:*)

View File

@ -1,4 +1,6 @@
from . import args
from . import ra
from . import tunnel
from . import config
from . import logger
from . import tunnelbroker

View File

@ -12,7 +12,7 @@ def parseArgs():
args.add_argument('-c', '--config',
dest = 'conf_xml',
default = '~/.config/he_tunnelbroker.xml',
help = ('The path to the config. See example.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',

View File

@ -1,132 +1,71 @@
import collections
import copy
import ipaddress
import os
import re
##
import netaddr
import requests
import requests.auth
from lxml import etree
from pyroute2 import IPRoute
##
from . import tunnel


class IP(object):
type = None
version = None
_ip = ipaddress.ip_address
_net = ipaddress.ip_network
_net_ip = netaddr.IPAddress
_net_net = netaddr.IPNetwork

def __init__(self, ip, prefix, *args, **kwargs):
self.str = ip
self.prefix = int(prefix)
self.net_ip = self._net_ip(self.str)
self.net_net = self._net_net('{0}/{1}'.format(self.str, self.prefix))

def _ext_init(self):
self.ip = self._ip(self.str)
self.net = self._net('{0}/{1}'.format(self.str, self.prefix), strict = False)
return(None)


class IP4(IP):
type = 'IPv4'
version = 4
_ip = ipaddress.IPv4Address
_net = ipaddress.IPv4Network

def __init__(self, ip, prefix, *args, **kwargs):
super().__init__(ip, prefix, *args, **kwargs)
self._ext_init()


class IP6(IP):
type = 'IPv6'
version = 6
_ip = ipaddress.IPv6Address
_net = ipaddress.IPv6Network

def __init__(self, ip, prefix, *args, **kwargs):
super().__init__(ip, prefix, *args, **kwargs)
self._ext_init()


class Allocation(object):
def __init__(self, alloc_xml):
self.xml = alloc_xml
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):
_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._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.parse()

def _allocations(self):
_allocs_xml = self.xml.find('allocs')
for _allocation_xml in _allocs_xml.findall('alloc'):
self.allocations.append(Allocation(_allocation_xml))
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()
return(None)
def create_default_cfg():
# Create a stripped sample config.
ws_re = re.compile(r'^\s*$')
cur_dir = os.path.dirname(os.path.abspath(os.path.expanduser(__file__)))
xtbxml = os.path.join(cur_dir, 'example.tunnelbroker.xml')
with open(xtbxml, 'rb') as fh:
xml = etree.fromstring(fh.read())
# Create a stripped sample config.
# First we strip comments (and fix the ensuing whitespace).
# etree has a .canonicalize(), but it chokes on a default namespace.
# https://bugs.launchpad.net/lxml/+bug/1869455
# So everything we do is kind of a hack.
# for c in xml.xpath("//comment()"):
# parent = c.getparent()
# parent.remove(c)
xmlstr = etree.tostring(xml, with_comments = False, method = 'c14n', pretty_print = True).decode('utf-8')
newstr = []
for line in xmlstr.splitlines():
r = ws_re.search(line)
if not r:
newstr.append(line.strip())
xml = etree.fromstring(''.join(newstr).encode('utf-8'))
# Remove text and attr text.
xpathq = "descendant-or-self::*[namespace-uri()!='']"
for e in xml.xpath(xpathq):
if e.tag == '{{{0}}}heIPv6'.format(xml.nsmap[None]):
continue
if e.text is not None and e.text.strip() != '':
e.text = ''
for k, v in e.attrib.items():
if v is not None:
e.attrib[k] = ''
# Remove multiple children of same type to simplify.
for e in xml.xpath(xpathq):
if e.tag == '{{{0}}}heIPv6'.format(xml.nsmap[None]):
continue
parent = e.getparent()
try:
for idx, child in enumerate(parent.findall(e.tag)):
if idx == 0:
continue
parent.remove(child)
except AttributeError:
pass
# And add a comment pointing them to the fully commented config.
xml.insert(0, etree.Comment(('\n Please reference the fully commented example.tunnelbroker.xml found either '
'at:\n '
' * {0}\n * https://git.square-r00t.net/RouterBox/tree/utils/he_ipv6/'
'example.tunnelbroker.xml\n and then configure this according to those '
'instructions.\n ').format(xtbxml)))
return(etree.tostring(xml,
pretty_print = True,
with_comments = True,
with_tail = True,
encoding = 'UTF-8',
xml_declaration = True))


class Credential(object):
@ -134,7 +73,7 @@ class Credential(object):
self.xml = cred_xml
self.id = None
self.user = None
self.key = None
self.password = None
self.parse()

def _id(self):
@ -144,14 +83,14 @@ class Credential(object):
self.id = _id.strip()
return(None)

def _update_key(self):
_key_xml = self.xml.find('updateKey')
def _password(self):
_key_xml = self.xml.find('password')
if _key_xml is None:
raise ValueError('Missing required updateKey element')
raise ValueError('Missing required password element')
_key_txt = _key_xml.text
if not _key_txt:
raise ValueError('updateKey element is empty')
self.key = _key_txt.strip()
raise ValueError('password element is empty')
self.password = _key_txt.strip()
return(None)

def _user(self):
@ -167,43 +106,23 @@ class Credential(object):
def parse(self):
self._id()
self._user()
self._update_key()
self._password()
return(None)


class Config(object):
default_xsd = 'http://schema.xml.r00t2.io/projects/he_ipv6.xsd'
class BaseConfig(object):
default_xsd = None

def __init__(self, xml_path, *args, **kwargs):
self.xml_path = os.path.abspath(os.path.expanduser(xml_path))
if not os.path.isfile(self.xml_path):
raise ValueError('xml_path does not exist')
def __init__(self, xml_raw, *args, **kwargs):
self.raw = xml_raw
self.tree = None
self.ns_tree = None
self.xml = None
self.ns_xml = None
self.raw = None
self.xsd = None
self.defaults_parser = None
self.obj = None
self.tunnels = collections.OrderedDict()
self.creds = {}
self.parse()

def _creds(self):
creds_xml = self.xml.find('creds')
for cred_xml in creds_xml.findall('cred'):
cred = Credential(cred_xml)
self.creds[cred.id] = cred
return(None)

def _tunnels(self):
tunnels_xml = self.xml.find('tunnels')
for tun_xml in tunnels_xml.findall('tunnel'):
tun = Tunnel(tun_xml)
tun.creds = self.creds.get(tun.creds_id)
self.tunnels[tun.id] = tun
return(None)
self.parse_xml()

def get_xsd(self):
raw_xsd = None
@ -230,18 +149,14 @@ class Config(object):
self.xsd = etree.XMLSchema(etree.XML(raw_xsd, base_url = base_url))
return(None)

def parse(self):
def parse_xml(self):
self.parse_raw()
self.get_xsd()
self.populate_defaults()
self.validate()
self.subparse()
return(None)

def parse_raw(self, parser = None):
if not self.raw:
with open(self.xml_path, 'rb') as fh:
self.raw = fh.read()
self.xml = etree.fromstring(self.raw, parser = parser)
self.ns_xml = etree.fromstring(self.raw, parser = parser)
self.tree = self.xml.getroottree()
@ -279,13 +194,198 @@ class Config(object):
raise ValueError('Did not know how to parse obj parameter')
return(None)

def subparse(self):
self._creds()
self._tunnels()
return(None)

def validate(self):
if not self.xsd:
self.get_xsd()
self.xsd.assertValid(self.ns_tree)
return(None)


class Config(BaseConfig):
default_xsd = 'http://schema.xml.r00t2.io/projects/router/he_ipv6.xsd'

def __init__(self, xml_path, *args, **kwargs):
self.xml_path = os.path.abspath(os.path.expanduser(xml_path))
if not os.path.isfile(self.xml_path):
with open(self.xml_path, 'wb') as fh:
fh.write(create_default_cfg())
raise ValueError('xml_path does not exist; '
'a sample configuration has been generated (be sure to configure it)')
else:
with open(xml_path, 'rb') as fh:
raw_xml = fh.read()
super().__init__(raw_xml, *args, **kwargs)
self.creds = {}
self.tunnels = collections.OrderedDict()
self.subparse()

def _creds(self):
creds_xml = self.xml.find('creds')
for cred_xml in creds_xml.findall('cred'):
cred = Credential(cred_xml)
self.creds[cred.id] = cred
return(None)

def _tunnels(self):
tunnels_xml = self.xml.find('tunnels')
for tun_xml in tunnels_xml.findall('tunnel'):
tun_id = int(tun_xml.attrib['id'].strip())
tun_creds_id = tun_xml.attrib['creds']
creds = self.creds[tun_creds_id]
update_key = tun_xml.find('updateKey').text.strip()
# TODO: do I instead want to use HEConfig() and fetch the single unified config?
# Pros:
# * I wouldn't completely die on a misconfigured tunnel in the user config.
# Cons:
# * We'd have to skip missing tunnels (bad auth at HE, etc.)
# * We would use more memory and take more time during init.
he_conf = HETunnelConfig(tun_id, creds, update_key)
tun = tunnel.Tunnel(tun_xml, he_conf, self.creds[tun_creds_id])
self.tunnels[tun_id] = tun
return(None)

def subparse(self):
self._creds()
self._tunnels()
return(None)


class HEBaseConfig(BaseConfig):
default_xsd = 'http://schema.xml.r00t2.io/projects/tunnelbroker.xsd'
nsmap = {None: 'https://tunnelbroker.net/tunnelInfo.php',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
attr_qname = etree.QName('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
schema_loc = 'https://tunnelbroker.net/tunnelInfo.php {0}'.format(default_xsd)
url = ''

def __init__(self, creds, *args, **kwargs):
self.creds = creds
super().__init__(self._fetch(), *args, **kwargs)

def _add_ns(self, raw_xml):
# https://mailman-mail5.webfaction.com/pipermail/lxml/20100323/013260.html
_xml = etree.fromstring(raw_xml)
_nsmap = copy.deepcopy(_xml.nsmap)
_nsmap.update(self.nsmap)
mod_xml = etree.Element(_xml.tag,
{self.attr_qname: self.schema_loc},
nsmap = _nsmap)
mod_xml[:] = _xml[:]
return(etree.tostring(mod_xml,
encoding = 'UTF-8',
xml_declaration = True,
pretty_print = True,
with_tail = True,
with_comments = True))

def _fetch(self):
req = requests.get(self.url,
auth = requests.auth.HTTPBasicAuth(self.creds.user,
self.creds.password))
if not req.ok:
raise RuntimeError('Could not fetch remote tunnel information')
raw_xml = self._add_ns(req.content)
return(raw_xml)


# This isn't really used.
class HEConfig(HEBaseConfig):
default_xsd = 'http://schema.xml.r00t2.io/projects/tunnelbroker.xsd'
nsmap = {None: 'https://tunnelbroker.net/tunnelInfo.php',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
attr_qname = etree.QName('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
schema_loc = 'https://tunnelbroker.net/tunnelInfo.php {0}'.format(default_xsd)
url = 'https://tunnelbroker.net/tunnelInfo.php'

def __init__(self, creds, *args, **kwargs):
super().__init__(creds, *args, **kwargs)
self.tunnels = {}

def add_tunnel(self, tun_id, update_key):
self.tunnels[tun_id] = HETunnelConfig(tun_id, self.creds, update_key)
return(None)


class HETunnelConfig(HEBaseConfig):
# default_xsd = 'http://schema.xml.r00t2.io/projects/tunnelbroker.tun.xsd'
# nsmap = {None: 'https://tunnelbroker.net/tunnelInfo.php?tid',
# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
# attr_qname = etree.QName('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation')
# schema_loc = 'https://tunnelbroker.net/tunnelInfo.php?tid {0}'.format(default_xsd)
url = 'https://tunnelbroker.net/tunnelInfo.php?tid={0}'

def __init__(self, tun_id, creds, update_key, *args, **kwargs):
self.tun_id = int(tun_id)
self.url = self.url.format(self.tun_id)
self.creds = copy.deepcopy(creds)
self.creds.password = update_key
super().__init__(self.creds, *args, **kwargs)
self.id = None
self.description = None
self.client = None # Client IPv6
self.server = None # Server IPv6
self.endpoint = None # Server IPv4
self.my_ip = None # Client IPv4 (not necessary; we locally cache Tunnel.my_ip)
self.allocations = {} # keys are 64 and 48
self.rdns = [] # Also not necessary, but it's in the XML so why not.
# Will only return a single <tunnel> for this URL.
# TODO: I can probably consolidate all this into HECond instead?
self.tun_xml = self.xml.find('tunnel')
self.parse()

def _alloc(self):
for a in ('64', '48'):
_alloc = self.tun_xml.find('routed{0}'.format(a))
if _alloc is not None and _alloc.text.strip() != '':
self.allocations[int(a)] = tunnel.Allocation(_alloc.text.strip())
return(None)

def _client(self):
_client = self.tun_xml.find('clientv6').text
if _client is not None and _client.strip() != '':
self.client = tunnel.IP6(_client.strip(), 64)
return(None)

def _desc(self):
_desc = self.tun_xml.find('description').text
if _desc is not None and _desc.strip() != '':
self.description = _desc.strip()
return(None)

def _endpoint(self):
self.endpoint = tunnel.IP4(self.tun_xml.find('serverv4').text.strip(), 32)
return(None)

def _id(self):
self.id = int(self.tun_xml.attrib['id'])
return(None)

def _my_ip(self):
_ip = self.tun_xml.find('clientv4').text
if _ip is not None and _ip.strip() != '':
self.my_ip = tunnel.IP4(_ip.strip(), 32)
return(None)

def _rdns(self):
self.rdns = []
for r in range(1, 6):
_rdns = self.tun_xml.find('rdns{0}'.format(r))
if _rdns is not None and _rdns.text.strip() != '':
self.rdns.append(_rdns.text.strip())
self.rdns = tuple(self.rdns)
return(None)

def _server(self):
self.server = tunnel.IP6(self.tun_xml.find('serverv6').text.strip(), 128)
return(None)

def parse(self):
self._id()
self._client()
self._desc()
self._endpoint()
self._server()
self._my_ip()
self._alloc()
self._rdns()
return(None)

View File

@ -1,85 +1,152 @@
<?xml version="1.0" encoding="UTF-8" ?>
<heIPv6 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://tunnelbroker.net/"
xsi:schemaLocation="https://tunnelbroker.net/ http://schema.xml.r00t2.io/projects/he_ipv6.xsd">
xsi:schemaLocation="https://tunnelbroker.net/ http://schema.xml.r00t2.io/projects/router/he_ipv6.xsd">
<!--
This is a sample XML configuration file to use with he_ipv6.py.
If you do not yet have an IPv6 Tunnelbroker.net allocation, you can get one (for free!) at:
https://www.tunnelbroker.net/tunnel_detail.php?tid=584532
I highly recommend their (free) certification as well if you're brand-new to IPv6:
https://ipv6.he.net/certification/
**It is VERY highly encouraged to only use one tunnel at a time on a machine. Completely unpredictable results will
incur if this is not heeded.**
-->
<creds>
<!--
Credentials are kept separate from tunnel configuration because you can have multiple (up to 5) tunnels per user.
The updateKey is *not* your password! You can find it in the "Advanced" tab of your tunnel's configuration on
your tunnelbroker.net panel.
-->
<cred id="ipv6user">
<user>ipv6user</user>
<updateKey>xXxXxXxXxXxXxXXX</updateKey>
<password>someSecretPassword</password>
</cred>
<cred id="anotheruser">
<user>someotheruser</user>
<updateKey>0000000000000000</updateKey>
<password>anotherPassword</password>
</cred>
</creds>
<tunnels>
<!--
Each tunnel MUST have an "id" and a "creds" attribute. The "creds" attribute should reference an "id" of a
creds/cred object.
The tunnel ID can be found by logging into your tunnelbroker.net pannel, clicking on the tunnel you wish to use, and
The tunnel ID can be found by logging into your tunnelbroker.net panel, clicking on the tunnel you wish to use, and
looking at the URL in your browser.
It is in the format of https://www.tunnelbroker.net/tunnel_detail.php?tid=[TUNNEL ID]
So if it takes you to e.g. https://www.tunnelbroker.net/tunnel_detail.php?tid=12345, your tunnel ID would
be "12345".
The below directives give you a Section and Value Name. This refers to the tunnelbroker.net panel page for the
specific tunnel you're configuring. e.g. To use the above example, this information is found at
https://www.tunnelbroker.net/tunnel_detail.php?tid=12345
-->
<tunnel id="12345" creds="ipv6user">
<!--
The "server" element is the remote SIT endpoint.
Section: IPv6 Tunnel Endpoints
Value Name: Server IPv4 Address
You can find the updateKey in the "Advanced" tab of your tunnel's configuration on your tunnelbroker.net panel.
-->
<server>192.0.2.1</server>
<updateKey>xXxXxXxXxXxXxXXX</updateKey>
<!--
Allocations that are handed to your tunnel.
Where to assign your allocations. The default allocation prefix is a /64 (prefix="64"), since that's what
SLAAC (RFC 2462) recommends.
It has one optional attribute, "raProvider", which can be "dnsmasq" or "radvd". Its configuration file will be
regenerated and the service restarted after the addresses are allocated to interfaces. Further system
configuration may be required. If not specified, the default is to not send router advertisements (RFC 4861). See
the "ra" child element under <assign> for further details.
If you are using dnsmasq, you will want to edit dnsmasq.conf to *include* the generated file, most likely, as it
only generates configuration for IPv6 options.
If this is not specified, NO RA/DHCPv6 management will be done *regardless* of any "re" child elements for below
"assign" objects.
-->
<!--
Section: Routed IPv6 Prefixes
-->
<allocs>
<assignments raProvider="dnsmasq">
<!--
Each alloc has (in addition to a "prefix" attribute) an "iface" attribute. This is the network interface on
this machine that the allocation should be added to.
Value Name: Routed /64
Each assignment has the following required attributes:
* "prefix" - the size of the subnet to assign to an interface, "64" (/64) by default since that's what SLAAC
recommends. Note that if you use your /64 allocation and don't specify a longer prefix, you can
only have one assignment for that allocation.
* "alloc" - this should match the prefix of the allocation. Hurricane Electric only allows you one /64 and,
optionally, one /48. Use "alloc" to reference which allocation you want to use. Uses "64" (/64)
by default.
* "iface" - which network interface on this machine the allocation should be added to.
Make sure you don't exceed your allocation size! (A /48 has 65536 /64s in it.)
The interface will be assigned :1 (the first host in the subnet) as well, so it is recommended that you do not
assign a /128 prefix.
-->
<alloc prefix="64" iface="eth0">2001:DB8:1:2::</alloc>
<!--
You may not have a /48 as it's opt-in.
Value Name: Routed /48
-->
<alloc prefix="48" iface="eth0">2001:DB8:2::</alloc>
</allocs>
<!--
The "client" element is the local SIT endpoint.
Section: IPv6 Tunnel Endpoints
Value Name: Client IPv6 Address
-->
<client prefix="64">2001:DB8:3::2</client>
<assign prefix="64" alloc="64" iface="eth0">
<!--
Each assignment CAN have an "ra" child. The default is to not implement RA for this interface if an ra element
is not present.
It takes one optional attribute, which is only used for raProvider="dnsmasq", "tag", which is the tag name for
the interface (this should be set in an earlier included conf/the main dnsmasq.conf).
-->
<ra tag="main">
<!--
Specify RDNSS (RFC 8106). If specified, this allocation's "router IP" (<PREFIX>::1) will be passed as a
resolver via RDNSS.
It takes one (optional) attribute, "domains", which is a space-separated list of search domains, referred
to in IPv6 as DNSSL (RFC 6106).
Note that Windows does not support DNSSL properly, and as such you must use dnsmasq as your RA provider if
you wish to send search domains.
If "domains" is specified but the element is false, the configuration will only advertise DNSSL and not
RDNSS.
If you also specify dhcpv6 below and are using dnsmasq as your raProvider, then:
* the same domains will be sent via DHCPv6 option 24
* the same RDNSS resolver will be passed via DHCPv6 option 23
-->
<dns domains="foo.com bar.com">true</dns>
<!--
Enable DHCPv6 for this assignment.

RADVD:
If you're using radvd, this will only enable the "AdvManagedFlag" and/or "AdvOtherConfigFlag" flags
(the "MO" bits). *No actual DHCPv6 address assignment will, or can, occur via radvd, only SLAAC.*

DNSMASQ:
To ensure maximum compatability with SLAAC, addresses will be served in the fixed range of:
<PREFIX>:dead:beef:cafe:[0000-FFFF]
(65535 addresses per prefix assignment, a.k.a. a /112).
Obviously your assignment's prefix length *must* be smaller than /112 (but should be at LEAST a /64 anyways
per RFC specification). Regardless of settings below, SLAAC *will* be offered if an "ra" element is
defined ("A" bit). Since we entirely deal with local links, the L bit is also always set.

It has an optional attribute, "advOther", which controls the "Other Configuration" bit.
The default is "false".
The "MO" bits (RFC 4861 § 4.2) are set accordingly:
===========================================================================================================
| Condition | M | O | A | L | Will addresses be assigned via DHCPv6 (if dnsmasq)? |
===========================================================================================================
| advOther="true", dhcpv6 is true | 1 | 1 | 1 | 1 | Yes |
| advOther="true", dhcpv6 is false | 0 | 1 | 1 | 1 | No |
| advOther="false", dhcpv6 is false | 0 | 0 | 1 | 1 | No |
| advOther="false", dhcpv6 is true | 1 | 0 | 1 | 1 | Yes (but O = 0 is pointless) |
===========================================================================================================
-->
<dhcpv6 advOther="true">true</dhcpv6>
</ra>
</assign>
<!-- Disable RA for this set (no "ra" child specified). -->
<assign prefix="64" alloc="48" iface="eth0"/>
<assign prefix="64" alloc="48" iface="eth1">
<ra tag="vmlan">
<!-- This will use strictly SLAAC (if using dnsmasq, obviously - radvd only does SLAAC). -->
<dhcpv6 advOther="false">false</dhcpv6>
<!-- And let clients choose their own resolver. -->
<dns>false</dns>
</ra>
</assign>
<assign prefix="64" alloc="48" iface="eth2">
<ra tag="wlan">
<!-- Only pass RDNSS resolvers. -->
<dns>true</dns>
<dhcpv6 advOther="false">false</dhcpv6>
</ra>
</assign>
</assignments>
</tunnel>
<!--
And you can, of course, specify multiple tunnels.
-->
<tunnel id="54321" creds="ipv6user">
<server>192.0.2.1</server>
<allocs>
<alloc prefix="64" iface="eth1">2001:DB8:4:2:</alloc>
<alloc prefix="48" iface="eth1">2001:DB8:5::</alloc>
</allocs>
<client prefix="64">2001:DB8:6::2</client>
<!-- And you can, of course, specify multiple tunnels. -->
<tunnel id="54321" creds="anotheruser">
<updateKey>0000000000000000</updateKey>
<assignments>
<!--
Uses the default prefix of /64 from your standard /64 allocation from Hurricane Electric.
Most users probably want this if they just want IPv6 for their local computer unless they're running an IPv6
router.
-->
<assign iface="eth0"/>
</assignments>
</tunnel>
</tunnels>
</heIPv6>

View File

@ -8,8 +8,10 @@ try:
except ImportError:
_has_journald = False


logfile = '/var/log/tunnelbroker_manager.log'
if os.geteuid() == 0:
logfile = '/var/log/tunnelbroker_manager.log'
else:
logfile = '~/.cache/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)

160
utils/he_ipv6/ra.py Normal file
View File

@ -0,0 +1,160 @@
import logging
import os
import subprocess
import warnings
##
import jinja2


logger = logging.getLogger()


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)


class RAConf(object):
cfgstr = None
tpl_dir = os.path.join(os.path.dirname(os.path.abspath(os.path.expanduser(__file__))), 'tpl')

def __init__(self, conf = None, tpl_name = None, tpl_dir = None, *args, **kwargs):
for k in ('name', 'dir'):
n = 'tpl_{0}'.format(k)
v = locals()[n]
if v:
setattr(self, n, v)
if conf:
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, extensions = ['jinja2.ext.do'])
self.tpl = self.tpl_env.get_template(self.tpl_name)
return(None)

def generate(self, assignments):
self.cfgstr = self.tpl.render(assignments = assignments)
return(None)

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):
is_systemd = False
cmd_tpl = None
has_pkill = False
start_cmd = None
stop_cmd = None
restart_cmd = None

def __init__(self, name):
self.name = name
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)
if s and s.decode('utf-8').strip() != '':
logger.warning('{0}: {1}'.format(i.upper(), s.decode('utf-8')))
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'
conf = '/etc/radvd.conf'
tpl_name = 'radvd.conf.j2'

def __init__(self, conf = None, tpl_name = None, tpl_dir = None):
if not conf:
conf = self.conf
if not tpl_name:
tpl_name = self.tpl_name
super().__init__(conf = conf, tpl_name = tpl_name, tpl_dir = tpl_dir)
self.svc = RASvc(self.name)
self.conf.ext_init()


class DNSMasq(RA):
name = 'dnsmasq'
conf = '/etc/dnsmasq.d/ra.conf'
tpl_name = 'dnsmasq.include.j2'

def __init__(self, conf = None, tpl_name = None, tpl_dir = None):
if not conf:
conf = self.conf
if not tpl_name:
tpl_name = self.tpl_name
super().__init__(conf = conf, tpl_name = tpl_name, tpl_dir = tpl_dir)
self.svc = RASvc(self.name)
self.conf.ext_init()

View File

@ -1,7 +1,41 @@
## General Info/Networking Configuration ##
# 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
# https://genneko.github.io/playing-with-bsd/networking/freebsd-tunnelv6-he
# https://journeymangeek.com/?p=228
# https://superuser.com/questions/1441598/using-a-hurricane-electric-tunnel-to-provide-ips-to-a-network-with-dnsmasq/1441604#1441604
# https://wiki.gentoo.org/wiki/IPv6_router_guide
# https://shorewall.org/6to4.htm#idm143
# https://shorewall.org/6to4.htm#SixInFour
# https://wiki.ubuntu.com/IPv6#Configure_your_Ubuntu_box_as_a_IPv6_router
# http://koo.fi/blog/2013/03/20/linux-ipv6-router-radvd-dhcpv6/

## Tunnelbroker API ##
# https://forums.he.net/index.php?topic=3153.0 ("The following scripts conform to the Dyn DNS Update API (as documented at http://dyn.com/support/developers/api/).")
# https://help.dyn.com/remote-access-api/return-codes/

## DNSMASQ ##
# https://hveem.no/using-dnsmasq-for-dhcpv6

## RFCs ##
# DNSSL #
# https://tools.ietf.org/html/rfc6106
# https://tools.ietf.org/html/rfc8106
# RDNSS #
# https://tools.ietf.org/html/rfc5006 (see also 6106, 8106)
# SLAAC
# https://tools.ietf.org/html/rfc2462
# https://tools.ietf.org/html/rfc4862
# https://tools.ietf.org/html/rfc8064
# Router Advertisements
# https://tools.ietf.org/html/rfc4861
# https://tools.ietf.org/html/rfc5175
# https://tools.ietf.org/html/rfc6104
# https://tools.ietf.org/html/rfc7772
# DHCPv6
# https://tools.ietf.org/html/rfc3315
# https://tools.ietf.org/html/rfc3646
# https://tools.ietf.org/html/rfc4649
# https://tools.ietf.org/html/rfc8415
##
# https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/ipv6_fhsec/configuration/xe-3s/ip6f-xe-3s-book/ip6-rfcs.pdf

View File

@ -0,0 +1,23 @@
{#- This is a set of common options between DNSMasq and RADVD. Or they're easier to just define here. -#}
{#- ## SLAAC OPTIONS ## -#}
{#- Is it 1480 or 1280? Arch wiki says 1480, but everything else (older) says 1280. -#}
{#- set mtu = 1280 -#}
{%- set mtu = 1480 -%}
{#- Minimum seconds allowed between sending unsolicited multicast RAs. 3 < x < (0.75 * max_inter) -#}
{#- If using Mobile Extensions, 0.33 < x (0.75 * max_inter) -#}
{%- set min_inter = 10 -%}
{#- Maximum seconds allowed between sending unsolicited multicast RAs. 4 < x < 1800 -#}
{#- If using Mobile Extensions, 0.07 < x 1800 -#}
{%- set max_inter = 60 -%}
{#- Minimum seconds between sending multicast RAs (solicited and unsolicited). -#}
{#- If using Mobile Extensions, 0.03 < x -#}
{%- set min_delay = 3 -%}
{#- The lifetime associated with the default router in units of seconds. 0 OR max_inter < x < 9000 -#}
{%- set lifetime = 9000 -%}
{#- ## DHCPv6 OPTIONS ## -#}
{#- Obviously, these only work for DNSMasq. -#}
{#- How long the lease should last until a new one is requested. -#}
{#- This is also used for *SLAAC addresses* in radvd. -#}
{%- set lease_life = 21600 -%}{#- 6 hours -#}
{#- How long should the options be valid for. -#}
{%- set opts_life = lease_life -%}

View File

@ -0,0 +1,55 @@
{%- import '_common.j2' as common_opts with context -%}
# This file should be *included* in your dnsmasq configuration.
# Generated by he_ipv6.
# See "dnsmasq --help dhcp6" for matching option identifers ("dhcp-option = ..., option6: <option>").
enable-ra
{% for assignment in assignments %}
{%- set assign_loop = loop -%}
{%- set ra_opts = [] -%}
{%- if assignment.ra_tag -%}
{%- set id_set = 'tag:' + assignment.ra_tag -%}
{%- set identifier = id_set -%}
{%- set do_listen = false -%}
{%- else -%}
{%- set id_set = 'set:' + assignment.iface -%}
{%- set identifier = 'tag:' + assignment.iface -%}
{%- set do_listen = true -%}
{%- endif -%}
{%- if assignment.ra_dhcp is false -%}
{%- do ra_opts.append('ra-only') -%}
{%- if assignment.ra_other is true -%}
{%- do ra_opts.append('ra-stateless') -%}
{%- endif -%}
{%- endif -%}
{%- do ra_opts.append('slaac') -%}
{%- do ra_opts.append('ra-names') -%}
# {{ assignment.iface }} assignment
# Assignment blocks:
{%- for b in assignment.iface_blocks %}
# * {{ b|string }}
{%- endfor %}
{%- if do_listen %}
listen-address = {{ assignment.iface_ll }}
{%- endif %}
ra-param = {{ assignment.iface }}, mtu:{{ common_opts.mtu }}, high, {{ common_opts.min_delay }}, {{ common_opts.lifetime }}
{%- if assignment.ra_dhcp %}
{%- for block in assignment.assign_objs %}
{%- set dhcp_range = block.dhcp6_range|join(', ') -%}
{%- if loop.index0 == 0 %}
dhcp-range = {{ id_set }}, {{ dhcp_range }}, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}
{%- else %}
dhcp-range = {{ identifier }}, {{ dhcp_range }}, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}
{%- endif %}
{%- endfor %}
{%- else %}
dhcp-range = {{ id_set }}, ::, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}{#- TODO: check this. #}
{%- endif %}
dhcp-option = {{ identifier }}, option6:information-refresh-time, {{ common_opts.opts_life }}
{%- if assignment.ra_dns %}
dhcp-option = {{ identifier }}, option6:dns-server, [{{ assignment.iface_ll }}]
{%- endif %}
{%- if assignment.ra_domains %}
dhcp-option = {{ identifier }}, option6:domain-search, {{ assignment.ra_domains|join(',') }}
{%- endif %}

{% endfor %}

View File

@ -0,0 +1,44 @@
{%- import '_common.j2' as common_opts with context -%}
# Generated by he_ipv6.
# This may go wonky with multiple assignments on the same iface.
{% for assignment in assignments %}
interface {{ assignment.iface }} {
AdvSendAdvert on;
AdvLinkMTU {{ common_opts.mtu }};
MinRtrAdvInterval {{ common_opts.min_inter }};
MaxRtrAdvInterval {{ common_opts.max_inter }};
MinDelayBetweenRAs {{ common_opts.min_delay }};
AdvDefaultLifetime {{ common_opts.lifetime }};
{%- if assignment.ra_dhcp is true -%}
AdvManagedFlag on;
{%- endif %}
{%- if assignment.ra_other is true -%}
AdvOtherConfigFlag on;
{%- endif %}

{%- for block in assignment.iface_blocks %}
prefix {{ block|string }} {
AdvOnLink on;
{%- if block.prefixlen <= 64 %}
AdvAutonomous on;
{%- endif %}
AdvValidLifetime {{ common_opts.lease_life }};
AdvPreferredLifetime {{ common_opts.lease_life }};
AdvRouterAddr off;
};
{%- endfor %}

{%- if assign.ra_dns is true %}
RDNSS {{ assignment.iface_ll }} {
AdvRDNSSLifetime {{ common_opts.opts_life }};
};
{%- endif %}

{%- if assign.ra_domains %}
DNSSL {{ assignment.ra_domains|join(' ') }} {
AdvDNSSLLifetime {{ common_opts.opts_life }};
};
{%- endif %}
};

{%- endfor %}

252
utils/he_ipv6/tunnel.py Normal file
View File

@ -0,0 +1,252 @@
import ipaddress
import logging
import re
import socket
##
import netaddr
from pyroute2 import IPRoute
##
from . import ra
from . import utils


# TODO: more logging
logger = logging.getLogger()


class IP(object):
type = None
version = None
_ip = ipaddress.ip_address
_net = ipaddress.ip_network
_net_ip = netaddr.IPAddress
_net_net = netaddr.IPNetwork

def __init__(self, ip, prefix, *args, **kwargs):
self.str = ip
self.prefix = int(prefix)
self.net_ip = self._net_ip(self.str)
self.net_net = self._net_net('{0}/{1}'.format(self.str, self.prefix))

def _ext_init(self):
self.ip = self._ip(self.str)
self.net = self._net('{0}/{1}'.format(self.str, self.prefix), strict = False)
return(None)


class IP4(IP):
type = 'IPv4'
version = 4
_ip = ipaddress.IPv4Address
_net = ipaddress.IPv4Network

def __init__(self, ip, prefix, *args, **kwargs):
super().__init__(ip, prefix, *args, **kwargs)
self._ext_init()


class IP6(IP):
type = 'IPv6'
version = 6
_ip = ipaddress.IPv6Address
_net = ipaddress.IPv6Network

def __init__(self, ip, prefix, *args, **kwargs):
super().__init__(ip, prefix, *args, **kwargs)
self._ext_init()
self.alloc_block = netaddr.SubnetSplitter(self.net_net)


class Assignment(object):
def __init__(self,
assign_xml,
ra_dns = False,
ra_dhcp = False,
ra_other = False,
ra_tag = None,
ra_domains = None):
self.xml = assign_xml
self.ra_dns = ra_dns
self.ra_tag = ra_tag
self.ra_other = ra_other
self.ra_dhcp = ra_dhcp
for a in ('dns', 'tag', 'other', 'dhcp'):
k = 'ra_{0}'.format(a)
v = getattr(self, k)
logger.debug('{0}: {1}'.format(k, v))
self.ra_domains = set()
if isinstance(ra_domains, list):
self.ra_domains.update(ra_domains)
elif isinstance(ra_domains, str):
self.ra_domains.update([i.lower().strip() for i in ra_domains if i.strip() != ''])
logger.debug('ra_domains: {0}'.format(', '.join(self.ra_domains)))
self.iface = None
self.iface_ll = None
self.iface_idx = None
self.iface_blocks = []
self.assign_objs = []
self.alloc = None # This must be set externally to a mapped Allocation instance
self.alloc_id = None
self.prefix = None
self.alloc_block = None
self.parse()

def _alloc(self):
self.alloc_id = int(self.xml.attrib['alloc'].strip())
return(None)

def _iface(self):
_iface_txt = self.xml.attrib['iface'].strip()
self.iface = _iface_txt.strip()
ipr = IPRoute()
logger.debug('Looking for iface {0}'.format(self.iface))
self.iface_idx = ipr.link_lookup(ifname = self.iface)[0]
logger.debug('Found iface {0} at idx {1}'.format(self.iface, self.iface_idx))
# Link-Local address
ll = ipr.get_addr(index = self.iface_idx,
family = socket.AF_INET6,
scope = 253)[0]['attrs']
addrs = dict(ll)['IFA_ADDRESS']
logger.debug('Link-Local address for {0}: {1}'.format(self.iface, addrs))
if isinstance(addrs, (list, tuple)):
addr = addrs[0]
else:
addr = addrs
self.iface_ll = addr
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
for block in self.alloc_block.extract_subnet(self.prefix, count = 1):
self.iface_blocks.append(block)
self.assign_objs.append(AsssignmentBlock(block))
logger.debug('Allocation blocks for {0}: {1}'.format(self.iface, ','.join([str(i) for i in self.iface_blocks])))
return(None)


class AsssignmentBlock(object):
def __init__(self, net_net):
self.ip, self.prefix = str(net_net).split('/')
self.prefix = int(self.prefix)
self.ip = IP6(self.ip, self.prefix)
if self.prefix > 64:
raise ValueError('Allocation/Assignment block must be a /64 or larger (i.e. a smaller prefix)')
# DHCPv6 range.
# We need to do some funky things here.
_base = self.ip.ip
_base = ipaddress.IPv6Address(re.sub(r'(:0000){4}$', r':dead:beef:cafe::', str(_base.exploded)))
self.base = re.sub(r':0$', r'', str(_base))
start = '{0}:0'.format(self.base)
stop = '{0}:ffff'.format(self.base)
self.dhcp6_range = (start, stop)


class Allocation(object):
def __init__(self, alloc_net):
_ip, _prefix = alloc_net.split('/')
self.id = int(_prefix.strip())
self.prefix = self.id
self.ip = IP6(_ip.strip(), self.prefix)


class Tunnel(object):
def __init__(self, tun_xml, he_tunnel, creds):
self.xml = tun_xml
self.creds = creds
self.he = he_tunnel
self.update_key = self.he.creds.password
self.id = None
self.client = None
self.server = None
self.endpoint = None
self.ra = None
self.ra_provider = None
self.allocations = {} # This is a dict of {}[alloc.id] = Allocation obj (as provided by HE)
self.assignments = [] # This is a list of Assignment objs
self.parse()

def _allocations(self):
self.allocations = self.he.allocations
return(None)

def _assignments(self):
_assigns_xml = self.xml.find('assignments')
self.ra_provider = _assigns_xml.attrib.get('raProvider')
for _assign_xml in _assigns_xml.findall('assign'):
do_dns = False
domains = []
do_dhcp = False
ra_other = False
tag = None
_ra_xml = _assign_xml.find('ra')
if _ra_xml is not None and self.ra_provider:
tag = _ra_xml.attrib.get('tag', None)
_dns_xml = _ra_xml.find('dns')
_dhcp_xml = _ra_xml.find('dhcpv6')
if _dns_xml is not None:
do_dns = utils.xml2bool(_dns_xml.text.strip())
domains = [i.strip() for i in _dns_xml.attrib.get('domains', '').split() if i.strip() != '']
if _dhcp_xml is not None:
do_dhcp = utils.xml2bool(_dhcp_xml.text.strip())
ra_other = utils.xml2bool(_dhcp_xml.attrib.get('advOther', 'false').strip())
assign = Assignment(_assign_xml,
ra_dns = do_dns,
ra_dhcp = do_dhcp,
ra_other = ra_other,
ra_tag = tag,
ra_domains = domains)
assign.alloc = self.allocations[assign.alloc_id]
assign.parse_alloc()
self.assignments.append(assign)
return(None)

def _client(self):
self.client = self.he.client
return(None)

def _creds(self):
self.creds_id = self.xml.attrib['creds'].strip()
return(None)

def _endpoint(self):
self.endpoint = self.he.endpoint
return(None)

def _id(self):
self.id = int(self.xml.attrib['id'].strip())
return(None)

def _ra(self):
# TODO: support conf path override via config XML?
if self.ra_provider.strip().lower() == 'dnsmasq':
self.ra = ra.DNSMasq()
elif self.ra_provider.strip().lower() == 'radvd':
self.ra = ra.RADVD()
self.ra.conf.generate(self.assignments)
return(None)

def _server(self):
self.server = self.he.server
return(None)

def parse(self):
self._id()
self._creds()
self._client()
self._server()
self._endpoint()
self._allocations()
self._assignments()
self._ra()
return(None)

View File

@ -1,3 +1,5 @@
import datetime
import json
import logging
import os
import socket
@ -8,13 +10,14 @@ 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'
def_rt_ip = '::192.88.99.1'
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))
@ -27,18 +30,27 @@ class TunnelBroker(object):
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 = config.IP4(req.json()['ip'], 32)
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.')
@ -47,15 +59,24 @@ class TunnelBroker(object):
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)
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):
if self.force_update:
logger.debug('IP update forced; updating.')
self._get_my_ip()
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:
@ -69,12 +90,15 @@ class TunnelBroker(object):
ifname = self.iface_name,
kind = 'sit',
sit_local = self.my_ip.str,
sit_remote = self.tun.server.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?): {1}'.format(self.iface_name, e))
'(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:
@ -109,36 +133,44 @@ 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))
except Exception as e:
logger.error(('Could not add default IPv6 route on link {0}: {1}').format(self.iface_name, 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 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:
# 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):
@ -172,16 +204,20 @@ class TunnelBroker(object):
ipr.close()
raise e
ipr.close()
self.tun.ra.svc.stop()
return(None)

def update(self, oneshot = False):
self._get_my_ip()
auth_handler = requests.auth.HTTPBasicAuth(self.tun.creds.user, self.tun.creds.key)
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},
'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))

10
utils/he_ipv6/utils.py Normal file
View File

@ -0,0 +1,10 @@
def xml2bool(xml_str):
if xml_str is None:
return(None)
xml_str = xml_str.lower()[0]
if xml_str in ('t', '1'):
return(True)
elif xml_str in ('f', '0'):
return(False)
else:
raise ValueError('Not a boolean value')

View File

@ -1,10 +1,11 @@
#!/usr/bin/env python3

import he_ipv6
import he_ipv6.logger


import logging
logger = logging.getLogger()
logger = logging.getLogger('HE Tunnelbroker Manager')


def main():
@ -16,7 +17,7 @@ def main():
elif _args.oper == 'stop':
tb.stop()
elif _args.oper == 'update':
tb.update(oneshot = True)
tb.update()
return(None)