From 822e171a0023c3f8f335cda08bcbf70b2d6d4070 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 28 Mar 2020 11:28:05 +0100
Subject: ipv6: T1831: migrate eui64 addressing to XML and python

---
 interface-definitions/include/ipv6-address.xml.i | 12 ++++++++
 python/vyos/ifconfig/interface.py                | 38 ++++++++++++++++++++++--
 python/vyos/util.py                              | 22 ++++++++++++++
 src/conf_mode/interfaces-bonding.py              |  7 +++++
 src/conf_mode/interfaces-bridge.py               |  7 +++++
 src/conf_mode/interfaces-ethernet.py             |  8 ++++-
 src/conf_mode/interfaces-l2tpv3.py               |  7 +++++
 src/conf_mode/interfaces-openvpn.py              |  7 +++++
 src/conf_mode/interfaces-pseudo-ethernet.py      |  7 +++++
 src/conf_mode/interfaces-vxlan.py                |  7 +++++
 src/conf_mode/interfaces-wireless.py             |  7 +++++
 11 files changed, 126 insertions(+), 3 deletions(-)

diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i
index 31629830e..507d5dcc1 100644
--- a/interface-definitions/include/ipv6-address.xml.i
+++ b/interface-definitions/include/ipv6-address.xml.i
@@ -6,5 +6,17 @@
         <valueless/>
       </properties>
     </leafNode>
+    <leafNode name="eui64">
+      <properties>
+        <help>ssign IPv6 address using EUI-64 based on MAC address</help>
+        <valueHelp>
+          <format>ipv6net</format>
+          <description>IPv6 address and prefix length</description>
+        </valueHelp>
+        <constraint>
+          <validator name="ipv6-prefix"/>
+        </constraint>
+      </properties>
+    </leafNode>
   </children>
 </node>
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index a2d202466..8b41d6158 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -21,10 +21,10 @@ import time
 from copy import deepcopy
 
 from vyos.validate import *     # should not * include
-from vyos.config import Config  # not used anymore
+from vyos.util import mac2eui64
 from vyos import ConfigError
 
-from ipaddress import IPv4Network, IPv6Address
+from ipaddress import IPv4Network, IPv6Address, IPv6Network
 from netifaces import ifaddresses, AF_INET, AF_INET6
 from time import sleep
 from os.path import isfile
@@ -393,6 +393,40 @@ class Interface(DHCP):
         """
         return self.set_interface('ipv6_autoconf', autoconf)
 
+    def set_ipv6_eui64_address(self, prefix):
+        """
+        Extended Unique Identifier (EUI), as per RFC2373, allows a host to
+        assign iteslf a unique IPv6 address based on a given IPv6 prefix.
+
+        If prefix is passed address is assigned, if prefix is '' address is
+        removed from interface.
+        """
+        # if prefix is an empty string convert it to None so mac2eui64 works
+        # as expected
+        if not prefix:
+            prefix = None
+
+        eui64 = mac2eui64(self.get_mac(), prefix)
+
+        if not prefix:
+            # if prefix is empty - thus removed - we need to walk through all
+            # interface IPv6 addresses and find the one with the calculated
+            # EUI-64 identifier. The address is then removed
+            for addr in self.get_addr():
+                addr_wo_prefix = addr.split('/')[0]
+                if is_ipv6(addr_wo_prefix):
+                    if eui64 in IPv6Address(addr_wo_prefix).exploded:
+                        self.del_addr(addr)
+
+            return None
+
+        # calculate and add EUI-64 IPv6 address
+        if IPv6Network(prefix):
+            # we also need to take the subnet length into account
+            prefix = prefix.split('/')[1]
+            eui64 = f'{eui64}/{prefix}'
+            self.add_addr(eui64 )
+
     def set_ipv6_forwarding(self, forwarding):
         """
         Configure IPv6 interface-specific Host/Router behaviour.
diff --git a/python/vyos/util.py b/python/vyos/util.py
index e8727c192..635b11ee5 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -207,3 +207,25 @@ def is_admin() -> bool:
     current_user = getuser()
     (_, _, _, admin_group_members) = getgrnam('sudo')
     return current_user in admin_group_members
+
+
+def mac2eui64(mac, prefix=None):
+    '''
+    Convert a MAC address to a EUI64 address or, with prefix provided, a full
+    IPv6 address.
+    Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
+    '''
+    # http://tools.ietf.org/html/rfc4291#section-2.5.1
+    eui64 = re.sub(r'[.:-]', '', mac).lower()
+    eui64 = eui64[0:6] + 'fffe' + eui64[6:]
+    eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
+
+    if prefix is None:
+        return ':'.join(re.findall(r'.{4}', eui64))
+    else:
+        try:
+            net = ip_network(prefix, strict=False)
+            euil = int('0x{0}'.format(eui64), 16)
+            return str(net[euil])
+        except:  # pylint: disable=bare-except
+            return
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index ab58b9159..cc119b91a 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -49,6 +49,7 @@ default_config_data = {
     'ip_proxy_arp': 0,
     'ip_proxy_arp_pvlan': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'intf': '',
@@ -196,6 +197,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         bond['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         bond['ipv6_forwarding'] = 0
@@ -433,6 +438,8 @@ def apply(bond):
         b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
         # IPv6 address autoconfiguration
         b.set_ipv6_autoconf(bond['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
         # IPv6 forwarding
         b.set_ipv6_forwarding(bond['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 1e143e30c..28e5957e4 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -47,6 +47,7 @@ default_config_data = {
     'ip_enable_arp_announce': 0,
     'ip_enable_arp_ignore': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'igmp_querier': 0,
@@ -159,6 +160,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         bridge['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         bridge['ipv6_forwarding'] = 0
@@ -275,6 +280,8 @@ def apply(bridge):
         br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
         # IPv6 address autoconfiguration
         br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
         # IPv6 forwarding
         br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index c7fabce60..286cab88e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -49,6 +49,7 @@ default_config_data = {
     'ip_proxy_arp': 0,
     'ip_proxy_arp_pvlan': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'intf': '',
@@ -174,6 +175,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         eth['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         eth['ipv6_forwarding'] = 0
@@ -341,9 +346,10 @@ def apply(eth):
         e.set_proxy_arp(eth['ip_proxy_arp'])
         # Enable private VLAN proxy ARP on this interface
         e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
-        # Disable IPv6 forwarding on this interface
         # IPv6 address autoconfiguration
         e.set_ipv6_autoconf(eth['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
         # IPv6 forwarding
         e.set_ipv6_forwarding(eth['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 18ae1f4d8..af1d3f482 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -34,6 +34,7 @@ default_config_data = {
     'local_port': 5000,
     'intf': '',
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'mtu': 1488,
@@ -108,6 +109,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         l2tpv3['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         l2tpv3['ipv6_forwarding'] = 0
@@ -210,6 +215,8 @@ def apply(l2tpv3):
         l.set_mtu(l2tpv3['mtu'])
         # IPv6 address autoconfiguration
         l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
         # IPv6 forwarding
         l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index a91cc2d73..17aa4697f 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -295,6 +295,7 @@ default_config_data = {
     'hash': '',
     'intf': '',
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'ping_restart': '60',
@@ -497,6 +498,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         openvpn['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         openvpn['ipv6_forwarding'] = 0
@@ -1056,6 +1061,8 @@ def apply(openvpn):
         o.set_alias(openvpn['description'])
         # IPv6 address autoconfiguration
         o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
         # IPv6 forwarding
         o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 4b1abc553..56d4fdfc3 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -46,6 +46,7 @@ default_config_data = {
     'ip_proxy_arp': 0,
     'ip_proxy_arp_pvlan': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'intf': '',
@@ -152,6 +153,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         peth['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         peth['ipv6_forwarding'] = 0
@@ -313,6 +318,8 @@ def apply(peth):
     p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
     # IPv6 address autoconfiguration
     p.set_ipv6_autoconf(peth['ipv6_autoconf'])
+    # IPv6 EUI-based address
+    p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
     # IPv6 forwarding
     p.set_ipv6_forwarding(peth['ipv6_forwarding'])
     # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 294864bc1..3d2638c6f 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -38,6 +38,7 @@ default_config_data = {
     'ip_enable_arp_ignore': 0,
     'ip_proxy_arp': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'link': '',
@@ -110,6 +111,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         vxlan['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # Disable IPv6 forwarding on this interface
     if conf.exists('ipv6 disable-forwarding'):
         vxlan['ipv6_forwarding'] = 0
@@ -218,6 +223,8 @@ def apply(vxlan):
         v.set_proxy_arp(vxlan['ip_proxy_arp'])
         # IPv6 address autoconfiguration
         v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
         # IPv6 forwarding
         v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index d368cfa5d..2c67c39ae 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -828,6 +828,7 @@ default_config_data = {
     'ip_enable_arp_announce': 0,
     'ip_enable_arp_ignore': 0,
     'ipv6_autoconf': 0,
+    'ipv6_eui64_prefix': '',
     'ipv6_forwarding': 1,
     'ipv6_dup_addr_detect': 1,
     'mac' : '',
@@ -1143,6 +1144,10 @@ def get_config():
     if conf.exists('ipv6 address autoconf'):
         wifi['ipv6_autoconf'] = 1
 
+    # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+    if conf.exists('ipv6 address eui64'):
+        wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
     # ARP enable ignore
     if conf.exists('ip enable-arp-ignore'):
         wifi['ip_enable_arp_ignore'] = 1
@@ -1504,6 +1509,8 @@ def apply(wifi):
         w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
         # IPv6 address autoconfiguration
         w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
+        # IPv6 EUI-based address
+        w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
         # IPv6 forwarding
         w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
         # IPv6 Duplicate Address Detection (DAD) tries
-- 
cgit v1.2.3