summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Poessinger <christian@poessinger.com>2021-06-12 22:49:09 +0200
committerGitHub <noreply@github.com>2021-06-12 22:49:09 +0200
commit5d687daba3a33e7619d4ec8cc08792e6c2bfa0c7 (patch)
tree61934beb5625aae26bf93df10f0210d7a01398ee
parent3a9041e2d4d4a48ba7c01439e69c5f86a4a850c2 (diff)
parent8ea648e482cfcb6e5dda945369ea10bb12dbdff7 (diff)
downloadvyos-1x-5d687daba3a33e7619d4ec8cc08792e6c2bfa0c7.tar.gz
vyos-1x-5d687daba3a33e7619d4ec8cc08792e6c2bfa0c7.zip
Merge pull request #875 from sarthurdev/dhcp_address_wait
ipsec: T1501: T3617: Add handling for missing addresses on boot when using dhcp-interface
-rw-r--r--data/templates/ipsec/ipsec.conf.tmpl2
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py32
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py36
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook76
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook.py46
5 files changed, 142 insertions, 50 deletions
diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl
index 342887883..53cba44b9 100644
--- a/data/templates/ipsec/ipsec.conf.tmpl
+++ b/data/templates/ipsec/ipsec.conf.tmpl
@@ -7,7 +7,7 @@ config setup
uniqueids = {{ "no" if disable_uniqreqids is defined else "yes" }}
{% if site_to_site is defined and site_to_site.peer is defined %}
-{% for peer, peer_conf in site_to_site.peer.items() %}
+{% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address %}
{% set peer_index = loop.index %}
{% set peer_ike = ike_group[peer_conf.ike_group] %}
{% set peer_esp = esp_group[peer_conf.default_esp_group] if peer_conf.default_esp_group is defined else None %}
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 4a3340ffb..820762fc2 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -20,6 +20,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.util import call, process_named_running, read_file
+ethernet_path = ['interfaces', 'ethernet']
tunnel_path = ['interfaces', 'tunnel']
nhrp_path = ['protocols', 'nhrp']
base_path = ['vpn', 'ipsec']
@@ -29,8 +30,39 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_delete(nhrp_path)
self.cli_delete(tunnel_path)
+ self.cli_delete(ethernet_path)
self.cli_commit()
+ def test_dhcp_fail_handling(self):
+ self.cli_delete(ethernet_path)
+ self.cli_delete(base_path)
+
+ # Interface for dhcp-interface
+ self.cli_set(ethernet_path + ['eth0', 'address', 'dhcp'])
+
+ # Set IKE/ESP Groups
+ self.cli_set(base_path + ["esp-group", "MyESPGroup", "proposal", "1", "encryption", "aes128"])
+ self.cli_set(base_path + ["esp-group", "MyESPGroup", "proposal", "1", "hash", "sha1"])
+ self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "dh-group", "2"])
+ self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "encryption", "aes128"])
+ self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "hash", "sha1"])
+
+ # Site to site
+ self.cli_set(base_path + ["ipsec-interfaces", "interface", "eth0"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "authentication", "mode", "pre-shared-secret"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "authentication", "pre-shared-secret", "MYSECRETKEY"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "ike-group", "MyIKEGroup"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "default-esp-group", "MyESPGroup"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "dhcp-interface", "eth0"])
+ self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "protocol", "gre"])
+
+ self.cli_commit()
+
+ ipsec_dhcp_waiting = read_file('/tmp/ipsec_dhcp_waiting')
+
+ self.assertIn('eth0', ipsec_dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook
+ self.assertTrue(process_named_running('charon')) # Commit should've still succeeded and launched charon
+
def test_site_to_site(self):
self.cli_delete(base_path)
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
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook
new file mode 100755
index 000000000..e00e5fe6e
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+if [ "$reason" == "REBOOT" ] || [ "$reason" == "EXPIRE" ]; then
+ exit 0
+fi
+
+DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting"
+
+if [ -f $DHCP_HOOK_IFLIST ] && [ "$reason" == "BOUND" ]; then
+ if grep -qw $interface $DHCP_HOOK_IFLIST; then
+ sudo rm $DHCP_HOOK_IFLIST
+ sudo python3 /usr/libexec/vyos/conf_mode/vpn_ipsec.py
+ exit 0
+ fi
+fi
+
+if [ "$old_ip_address" == "$new_ip_address" ] && [ "$reason" == "BOUND" ]; then
+ exit 0
+fi
+
+python3 - <<PYEND
+import os
+import re
+from vyos.util import call
+from vyos.util import cmd
+
+IPSEC_CONF="/etc/ipsec.conf"
+IPSEC_SECRETS="/etc/ipsec.secrets"
+
+def getlines(file):
+ with open(file, 'r') as f:
+ return f.readlines()
+
+def writelines(file, lines):
+ with open(file, 'w') as f:
+ f.writelines(lines)
+
+def ipsec_down(ip_address):
+ # This prevents the need to restart ipsec and kill all active connections, only the stale connection is closed
+ status = cmd('sudo ipsec statusall')
+ connection_name = None
+ for line in status.split("\n"):
+ if line.find(ip_address) > 0:
+ regex_match = re.search(r'(peer-[^:\[]+)', line)
+ if regex_match:
+ connection_name = regex_match[1]
+ break
+ if connection_name:
+ call(f'sudo ipsec down {connection_name}')
+
+if __name__ == '__main__':
+ interface = os.getenv('interface')
+ new_ip = os.getenv('new_ip_address')
+ old_ip = os.getenv('old_ip_address')
+
+ conf_lines = getlines(IPSEC_CONF)
+ secrets_lines = getlines(IPSEC_SECRETS)
+ found = False
+ to_match = f'# dhcp:{interface}'
+
+ for i, line in enumerate(conf_lines):
+ if line.find(to_match) > 0:
+ conf_lines[i] = line.replace(old_ip, new_ip)
+ found = True
+
+ for i, line in enumerate(secrets_lines):
+ if line.find(to_match) > 0:
+ secrets_lines[i] = line.replace(old_ip, new_ip)
+
+ if found:
+ writelines(IPSEC_CONF, conf_lines)
+ writelines(IPSEC_SECRETS, secrets_lines)
+ ipsec_down(old_ip)
+ call('sudo /usr/sbin/ipsec rereadall')
+ call('sudo /usr/sbin/ipsec reload')
+PYEND \ No newline at end of file
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook.py b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook.py
deleted file mode 100644
index 36edf04f3..000000000
--- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-import sys
-
-from vyos.util import call
-
-IPSEC_CONF="/etc/ipsec.conf"
-IPSEC_SECRETS="/etc/ipsec.secrets"
-
-def getlines(file):
- with open(file, 'r') as f:
- return f.readlines()
-
-def writelines(file, lines):
- with open(file, 'w') as f:
- f.writelines(lines)
-
-if __name__ == '__main__':
- interface = os.getenv('interface')
- new_ip = os.getenv('new_ip_address')
- old_ip = os.getenv('old_ip_address')
- reason = os.getenv('reason')
-
- if (old_ip == new_ip and reason != 'BOUND') or reason in ['REBOOT', 'EXPIRE']:
- sys.exit(0)
-
- conf_lines = getlines(IPSEC_CONF)
- secrets_lines = getlines(IPSEC_SECRETS)
- found = False
- to_match = f'# dhcp:{interface}'
-
- for i, line in enumerate(conf_lines):
- if line.find(to_match) > 0:
- conf_lines[i] = line.replace(old_ip, new_ip)
- found = True
-
- for i, line in enumerate(secrets_lines):
- if line.find(to_match) > 0:
- secrets_lines[i] = line.replace(old_ip, new_ip)
-
- if found:
- writelines(IPSEC_CONF, conf_lines)
- writelines(IPSEC_SECRETS, secrets_lines)
- call('sudo /usr/sbin/ipsec rereadall')
- call('sudo /usr/sbin/ipsec reload')