okay. let's give this a shot.

This commit is contained in:
brent s. 2020-05-18 04:59:00 -04:00
parent 4df9287abd
commit 80765e58ed
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
10 changed files with 203 additions and 81 deletions

View File

@ -1,4 +1,3 @@
* fix creds
** needs user/password, and the updateKey is unique per-tunnel so move it into an element in there.
** need to get user/password into HEConf somehow. if i can get ?tid= working for the URL, that'd be perfect.
^ need to use updateKey for tunnel-specific xml
DHCPv6:
* NTP server in <ra>? (dnsmasq: option6:ntpserver,...)
* bootfile-(url|param) in <ra>? (dnsmasq: option6:*)

View File

@ -233,6 +233,12 @@ class Config(BaseConfig):
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

View File

@ -48,6 +48,8 @@
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.
-->
<assignments raProvider="dnsmasq">
<!--
@ -76,27 +78,50 @@
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, and as such you must use dhcpv6's "domains" attribute if you wish
to do that.
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. Only used for dnsmasq, has no effect for radvd. As mentioned above, you
can also specify the "domains" attribute here as well, which will pass them via a regular DHCPv6 option.
If "domains" is specified but the element is false, only the domains will be passed.
Again, this only pertains to dnsmasq since radvd offers no DHCPv6 capabilities whatsoever.
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.

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 | Will addresses be assigned via DHCPv6 (if dnsmasq)? |
===================================================================================================
| advOther="true", dhcpv6 is true | 1 | 1 | Yes |
| advOther="true", dhcpv6 is false | 0 | 1 | No |
| advOther="false", dhcpv6 is false | 0 | 0 | No |
| advOther="false", dhcpv6 is true | 1 | 0 | Yes |
===================================================================================================
-->
<dhcpv6 domains="foo.com bar.com">true</dhcpv6>
<dhcpv6 advOther="true">true</dhcpv6>
</ra>
</assign>
<!-- Disable RA for this set (no "ra" chiled specified). -->
<!-- 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>false</dhcpv6>
<dhcpv6 advOther="false">false</dhcpv6>
<!-- And let clients choose their own resolver. -->
<dns>false</dns>
</ra>
@ -105,7 +130,7 @@
<ra tag="wlan">
<!-- Only pass RDNSS resolvers. -->
<dns>true</dns>
<dhcpv6>false</dhcpv6>
<dhcpv6 advOther="false">false</dhcpv6>
</ra>
</assign>
</assignments>
@ -116,7 +141,8 @@
<assignments>
<!--
Uses the default prefix of /64 from your standard /64 allocation from Hurricane Electric.
Most users probably want this unless they're running an IPv6 router.
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>

View File

@ -43,14 +43,8 @@ class RAConf(object):
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)
self.cfgstr = self.tpl.render(assignments = assignments)
return(None)

def write(self):
if not self.cfgstr:

View File

@ -32,3 +32,10 @@
# 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

@ -5,12 +5,19 @@
{%- 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 = 60 -%}
{%- 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 = 600 -%}
{%- 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 works for DNSMasq. -#}
{#- 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

@ -1,2 +1,49 @@
{%- 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_dns -%}
{%- do ra_opts.append('sa-names') -%}
{%- 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
{%- if do_listen %}
listen = {{ 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.iface_blocks %}
dhcp-range = {{ id_set }}, {{ assignment.dhcp6_ranges[assign_loop.index0]|join(', ') }}, {{ ra_opts|join(', ') }}, {{ common_opts.lease_life }}
{%- endfor %}
{%- else %}
dhcp-range = {{ id_set }}, ::, constructor={{ assignment.iface }}, {{ 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

@ -1,30 +1,44 @@
# Generated by he_ipv6
{% for assign in assignments %}
{% for assignment in assign.iface_blocks %}
{%- 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;
# 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;
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 %}
{%- if block.prefixlen <= 64 %}
AdvAutonomous on;
{%- endif %}
AdvValidLifetime {{ common_opts.lease_life }};
AdvPreferredLifetime {{ common_opts.lease_life }};
AdvRouterAddr off;
};
{%- endfor %}

{%- if ra.dns is true %}
RDNSS {{ nameservers[assignment.iface]|join(' ') }} {
{%- 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 %}
{%- endfor %}

View File

@ -1,10 +1,11 @@
import ipaddress
import socket
##
import netaddr
from pyroute2 import IPRoute
##
from . import utils
from . import ra
from . import utils


class IP(object):
@ -51,14 +52,28 @@ class IP6(IP):


class Assignment(object):
def __init__(self, assign_xml, ra = False, dns = False, ra_provider = 'dnsmasq'):
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 = ra
self.dns = dns
self.ra_dns = ra_dns
self.ra_tag = ra_tag
self.ra_other = ra_other
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() != ''])
self.ra_dhcp = ra_dhcp
self.iface = None
self.iface_ll = None
self.iface_idx = None
self.iface_addrs = []
self.iface_blocks = []
self.dhcp6_ranges = []
self.alloc = None # This must be set externally to a mapped Allocation instance
self.alloc_id = None
self.prefix = None
@ -74,6 +89,16 @@ class Assignment(object):
self.iface = _iface_txt.strip()
ipr = IPRoute()
self.iface_idx = ipr.link_lookup(ifname = self.iface)[0]
# Link-Local address
ll = ipr.get_addr(index = self.iface_idx,
family = socket.AF_INET6,
scope = 253)[0]['attrs']
addrs = dict(ll)['IFA_ADDRESS']
if isinstance(addrs, (list, tuple)):
addr = addrs[0]
else:
addr = addrs
self.iface_ll = addr
ipr.close()
return(None)

@ -92,7 +117,11 @@ class Assignment(object):
# NOT AN IP6 OBJECT!
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))
# DHCPv6 range.
_base = str(i.ip).rstrip(':')
start = '{0}:dead:beef:cafe:0'.format(_base)
stop = '{0}:dead:beef:cafe:ffff'.format(_base)
self.dhcp6_ranges.append((start, stop))
return(None)


@ -114,11 +143,8 @@ class Tunnel(object):
self.client = None
self.server = None
self.endpoint = 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.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()

@ -128,11 +154,27 @@ class Tunnel(object):

def _assignments(self):
_assigns_xml = self.xml.find('assignments')

self.enable_ra = utils.xml2bool(_assigns_xml.attrib.get('radvd', 'false'))
self.ra_dns = utils.xml2bool(_assigns_xml.attrib.get('radvdDns', 'false'))
self.ra_provider = _assigns_xml.attrib.get('raProvider')
for _assign_xml in _assigns_xml.findall('assign'):
assign = Assignment(_assign_xml, ra = self.enable_ra, dns = self.ra_dns)
do_dns = False
domains = []
do_dhcp = False
ra_other = False
tag = _assign_xml.attrib.get('tag', None)
dns = _assign_xml.find('dns')
if dns and self.ra_provider:
do_dns = utils.xml2bool(dns.text.strip())
domains = [i.strip() for i in dns.attrib.get('domains', '').split() if i.strip() != '']
dhcp = _assign_xml.find('dhcpv6')
if dhcp and self.ra_provider:
do_dhcp = utils.xml2bool(dhcp.text.strip())
ra_other = utils.xml2bool(dhcp.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)
@ -154,9 +196,13 @@ class Tunnel(object):
self.id = int(self.xml.attrib['id'].strip())
return(None)

def _radvd(self):

self.radvd.conf.generate(self.assignments)
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):
@ -171,5 +217,5 @@ class Tunnel(object):
self._endpoint()
self._allocations()
self._assignments()
self._radvd()
self._ra()
return(None)

View File

@ -143,30 +143,6 @@ class TunnelBroker(object):
ipr.close()
raise e
for assignment in self.tun.assignments:
for a in assignment.iface_addrs:
# The interface-specific ":1" addrs.
# Try to remove first in case it's already assigned.
try:
ipr.addr('del',
index = assignment.iface_idx,
address = a.str,
mask = a.prefix,
family = socket.AF_INET6)
logger.debug('Removed {0} with prefix {1} from {2}.'.format(a.str, a.prefixlen, assignment.iface))
except Exception as e:
pass
try:
ipr.addr('add',
index = assignment.iface_idx,
address = a.str,
mask = a.prefix,
family = socket.AF_INET6)
logger.debug('Added {0} with prefix {1} to {2}.'.format(a.str, a.prefix, assignment.iface))
except Exception as e:
logger.error(('Could not add address {0} on {1}: '
'{2}').format(a.str, assignment.iface, e))
ipr.close()
raise e
# The SLAAC prefixes.
for b in assignment.iface_blocks:
# Try to remove first in case it's already assigned.