From ab5bfe76044f7e5bb7921f11e735dbf77544a89f Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Fri, 11 Jun 2021 23:55:24 +0200
Subject: ipsec: T1501: T3617: Add handling for missing addresses on boot when
 using dhcp-interface

---
 src/conf_mode/vpn_ipsec.py | 36 +++++++++++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 3 deletions(-)

(limited to 'src/conf_mode')

diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 4efedd995..3eaa78a4b 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -23,6 +23,7 @@ from vyos.config import Config
 from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_interface_exists
 from vyos.ifconfig import Interface
+from vyos.template import ip_from_cidr
 from vyos.template import render
 from vyos.util import call
 from vyos.util import dict_search
@@ -73,12 +74,16 @@ any_log_modes = [
 ike_ciphers = {}
 esp_ciphers = {}
 
+dhcp_wait_attempts = 2
+dhcp_wait_sleep = 1
+
 mark_base = 0x900000
 
 CA_PATH = "/etc/ipsec.d/cacerts/"
 CRL_PATH = "/etc/ipsec.d/crls/"
 
 DHCP_BASE = "/var/lib/dhcp/dhclient"
+DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting"
 
 LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/']
 X509_PATH = '/config/auth/'
@@ -96,6 +101,7 @@ def get_config(config=None):
     ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  get_first_key=True, no_tag_node_value_mangle=True)
 
+    ipsec['dhcp_no_address'] = {}
     ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface'])
     ipsec['l2tp_exists'] = conf.exists('vpn l2tp remote-access ipsec-settings ')
     ipsec['nhrp_exists'] = conf.exists('protocols nhrp tunnel')
@@ -162,6 +168,15 @@ def verify_rsa_local_key(ipsec):
 def verify_rsa_key(ipsec, key_name):
     return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys'])
 
+def get_dhcp_address(iface):
+    addresses = Interface(iface).get_addr()
+    if not addresses:
+        return None
+    for address in addresses:
+        if not address.startswith("fe80:"): # Skip link-local ipv6
+            return ip_from_cidr(address)
+    return None
+
 def verify(ipsec):
     if not ipsec:
         return None
@@ -252,9 +267,17 @@ def verify(ipsec):
                 if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'):
                     raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
 
-                address = Interface(dhcp_interface).get_addr()
+                address = get_dhcp_address(dhcp_interface)
+                count = 0
+                while not address and count < dhcp_wait_attempts:
+                    address = get_dhcp_address(dhcp_interface)
+                    count += 1
+                    sleep(dhcp_wait_sleep)
+
                 if not address:
-                    raise ConfigError(f"Failed to get address from dhcp-interface on site-to-site peer {peer}")
+                    ipsec['dhcp_no_address'][peer] = dhcp_interface
+                    print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped")
+                    continue
 
             if 'vti' in peer_conf:
                 if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
@@ -291,6 +314,10 @@ def generate(ipsec):
     data = {}
 
     if ipsec:
+        if ipsec['dhcp_no_address']:
+            with open(DHCP_HOOK_IFLIST, 'w') as f:
+                f.write(" ".join(ipsec['dhcp_no_address'].values()))
+
         data = ipsec
         data['authby'] = authby_translate
         data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers}
@@ -300,6 +327,9 @@ def generate(ipsec):
 
         if 'site_to_site' in data and 'peer' in data['site_to_site']:
             for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+                if peer in ipsec['dhcp_no_address']:
+                    continue
+
                 if peer_conf['authentication']['mode'] == 'x509':
                     ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file'])
                     call(f'cp -f {ca_cert_file} {CA_PATH}')
@@ -312,7 +342,7 @@ def generate(ipsec):
                 if 'local_address' in peer_conf:
                     local_ip = peer_conf['local_address']
                 elif 'dhcp_interface' in peer_conf:
-                    local_ip = Interface(peer_conf['dhcp_interface']).get_addr()
+                    local_ip = get_dhcp_address(peer_conf['dhcp_interface'])
 
                 data['site_to_site']['peer'][peer]['local_address'] = local_ip
 
-- 
cgit v1.2.3