From f7834324d3d9edd7e161e7f2f3868452997c9c81 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Thu, 28 Dec 2023 22:11:26 -0800 Subject: T5872: ipsec remote access VPN: support dhcp-interface. --- smoketest/scripts/cli/test_vpn_ipsec.py | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) (limited to 'smoketest') diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 09e10a2c4..00f9e8f46 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -898,5 +898,66 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.tearDownPKI() + def test_remote_access_dhcp_fail_handling(self): + # Skip process check - connection is not created for this test + self.skip_process_check = True + + # Interface for dhcp-interface + self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server + + # This is a known to be good configuration for Microsoft Windows 10 and Apple iOS 17 + self.setupPKI() + + ike_group = 'IKE-RW' + esp_group = 'ESP-RW' + + conn_name = 'vyos-rw' + ip_pool_name = 'ra-rw-ipv4' + username = 'vyos' + password = 'secret' + ike_lifetime = '7200' + eap_lifetime = '3600' + local_id = 'ipsec.vyos.net' + + name_server = '172.16.254.100' + prefix = '172.16.250.0/28' + + # IKE + self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', ike_lifetime]) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512']) + + # ESP + self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime]) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256']) + self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512']) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-users', 'username', username, 'password', password]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509']) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name]) + + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group]) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'dhcp-interface', f'{interface}.{vif}']) + self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name]) + self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', name_server]) + self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix]) + + self.cli_commit() + + self.assertTrue(os.path.exists(dhcp_waiting_file)) + + dhcp_waiting = read_file(dhcp_waiting_file) + self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook + + self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address']) + + self.tearDownPKI() + if __name__ == '__main__': unittest.main(verbosity=2) -- cgit v1.2.3 From 679b78356cbda4de15f96a7f22d4a98037dbeea4 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sun, 10 Mar 2024 11:39:19 -0700 Subject: T5872: re-write exit hook to always regenerate config --- data/templates/ipsec/swanctl/peer.j2 | 2 +- data/templates/ipsec/swanctl/remote_access.j2 | 2 +- smoketest/scripts/cli/test_vpn_ipsec.py | 14 +++--- src/conf_mode/vpn_ipsec.py | 11 ++-- .../dhclient-exit-hooks.d/99-ipsec-dhclient-hook | 58 ++++++---------------- 5 files changed, 32 insertions(+), 55 deletions(-) (limited to 'smoketest') diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index f38691884..58f0199fa 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -8,7 +8,7 @@ {% if peer_conf.virtual_address is vyos_defined %} vips = {{ peer_conf.virtual_address | join(', ') }} {% endif %} - local_addrs = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '%any' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} reset:{{ name }} + local_addrs = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '%any' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} remote_addrs = {{ peer_conf.remote_address | join(",") if peer_conf.remote_address is vyos_defined and 'any' not in peer_conf.remote_address else '%any' }} {% if peer_conf.authentication.mode is vyos_defined('x509') %} send_cert = always diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2 index d1d6d2478..af7f2994e 100644 --- a/data/templates/ipsec/swanctl/remote_access.j2 +++ b/data/templates/ipsec/swanctl/remote_access.j2 @@ -4,7 +4,7 @@ {% set esp = esp_group[rw_conf.esp_group] %} ra-{{ name }} { remote_addrs = %any - local_addrs = {{ rw_conf.local_address if rw_conf.local_address is not vyos_defined('any') else '%any' }} # dhcp:{{ rw_conf.dhcp_interface if rw_conf.dhcp_interface is vyos_defined else 'no' }} reset:ra-{{ name }} + local_addrs = {{ rw_conf.local_address if rw_conf.local_address is not vyos_defined('any') else '%any' }} # dhcp:{{ rw_conf.dhcp_interface if rw_conf.dhcp_interface is vyos_defined else 'no' }} proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }} version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }} send_certreq = no diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 00f9e8f46..1a5e47144 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -31,7 +31,7 @@ nhrp_path = ['protocols', 'nhrp'] base_path = ['vpn', 'ipsec'] charon_file = '/etc/strongswan.d/charon.conf' -dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting' +dhcp_interfaces_file = '/tmp/ipsec_dhcp_interfaces' swanctl_file = '/etc/swanctl/swanctl.conf' peer_ip = '203.0.113.45' @@ -178,10 +178,10 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_commit() - self.assertTrue(os.path.exists(dhcp_waiting_file)) + self.assertTrue(os.path.exists(dhcp_interfaces_file)) - dhcp_waiting = read_file(dhcp_waiting_file) - self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook + dhcp_interfaces = read_file(dhcp_interfaces_file) + self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address']) @@ -950,10 +950,10 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_commit() - self.assertTrue(os.path.exists(dhcp_waiting_file)) + self.assertTrue(os.path.exists(dhcp_interfaces_file)) - dhcp_waiting = read_file(dhcp_waiting_file) - self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook + dhcp_interfaces = read_file(dhcp_interfaces_file) + self.assertIn(f'{interface}.{vif}', dhcp_interfaces) # Ensure dhcp interface was added for dhclient hook self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address']) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 64d0f6d9d..ebfb21903 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -75,7 +75,7 @@ KEY_PATH = f'{swanctl_dir}/private/' CA_PATH = f'{swanctl_dir}/x509ca/' CRL_PATH = f'{swanctl_dir}/x509crl/' -DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' +DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_interfaces' def get_config(config=None): if config: @@ -94,6 +94,7 @@ def get_config(config=None): with_recursive_defaults=True, with_pki=True) + ipsec['dhcp_interfaces'] = set() ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) @@ -241,6 +242,8 @@ def verify(ipsec): if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}") + ipsec['dhcp_interfaces'].add(dhcp_interface) + address = get_dhcp_address(dhcp_interface) count = 0 while not address and count < dhcp_wait_attempts: @@ -410,6 +413,8 @@ def verify(ipsec): if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}") + ipsec['dhcp_interfaces'].add(dhcp_interface) + address = get_dhcp_address(dhcp_interface) count = 0 while not address and count < dhcp_wait_attempts: @@ -527,9 +532,9 @@ def generate(ipsec): render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes}) return - if ipsec['dhcp_no_address']: + if ipsec['dhcp_interfaces']: with open(DHCP_HOOK_IFLIST, 'w') as f: - f.write(" ".join(ipsec['dhcp_no_address'].values())) + f.write(" ".join(ipsec['dhcp_interfaces'])) elif os.path.exists(DHCP_HOOK_IFLIST): os.unlink(DHCP_HOOK_IFLIST) diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook index 3f0c9cb7a..ebb100e8b 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook @@ -14,60 +14,32 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting" +DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_interfaces" -if [ -f $DHCP_HOOK_IFLIST ] && [ "$reason" == "BOUND" ]; then - if grep -qw $interface $DHCP_HOOK_IFLIST; then - sudo rm $DHCP_HOOK_IFLIST - sudo /usr/libexec/vyos/conf_mode/vpn_ipsec.py - exit 0 - fi +if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then + exit 0 fi +# Re-generate the config on the following events: +# - BOUND: always re-generate +# - RENEW: re-generate if the IP address changed +# - REBIND: re-generate if the IP address changed if [ "$reason" == "RENEW" ] || [ "$reason" == "REBIND" ]; then if [ "$old_ip_address" == "$new_ip_address" ]; then exit 0 fi -else +elif [ "$reason" != "BOUND" ]; then exit 0 fi -python3 - < 0: - conf_lines[i] = line.replace(old_ip, new_ip) - found = True - regex_match = re.search(r'#.* reset:([-_a-zA-Z0-9|@]+)', line) - if regex_match: - connection_name = regex_match[1] - reset_conns.add(connection_name) - - if found: - write_file(SWANCTL_CONF, "\n".join(conf_lines)) - for connection_name in reset_conns: - call(f'sudo swanctl -t -i {connection_name}') - call('sudo swanctl -q') - + wait_for_commit_lock() exit(0) PYEND + +# Now re-generate the config +sudo /usr/libexec/vyos/conf_mode/vpn_ipsec.py -- cgit v1.2.3