diff options
| author | Viacheslav Hletenko <v.gletenko@vyos.io> | 2026-03-04 12:01:50 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-04 12:01:50 +0200 |
| commit | 2a4d52b2b8c39f4f77b526ce80686247dd4d0f95 (patch) | |
| tree | edcf61a295a71c50d8ca20c22a8bec3378f5e86d /src | |
| parent | 3030e33b26c1a895179747f2444a0cd79d59f891 (diff) | |
| parent | 673ec335b885ae1de8391dd8c48a5fe16b282db5 (diff) | |
| download | vyos-1x-2a4d52b2b8c39f4f77b526ce80686247dd4d0f95.tar.gz vyos-1x-2a4d52b2b8c39f4f77b526ce80686247dd4d0f95.zip | |
Merge pull request #4930 from giga1699/T8136
ipsec: T8136: IPSEC PPK support
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 59 | ||||
| -rwxr-xr-x | src/op_mode/ipsec.py | 44 |
2 files changed, 101 insertions, 2 deletions
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index c6492f513..3b5918218 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -54,6 +54,7 @@ from vyos.utils.vti_updown_db import vti_updown_db_exists from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update from vyos.utils.vti_updown_db import remove_vti_updown_db from vyos import ConfigError +from vyos.base import Warning from vyos import airbag airbag.enable() @@ -261,11 +262,29 @@ def verify(ipsec): if not ipsec or 'deleted' in ipsec: return + # T8136 PPK support; keep a list of PPK IDs + ppk_ids = [] + if 'authentication' in ipsec: if 'psk' in ipsec['authentication']: for psk, psk_config in ipsec['authentication']['psk'].items(): if 'id' not in psk_config or 'secret' not in psk_config: - raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') + raise ConfigError( + f'Authentication psk "{psk}" missing "id" or "secret"' + ) + # T8136 PPK Support; Check that PPK has an ID and secret defined, and ID is unique + if 'ppk' in ipsec['authentication']: + for ppk, ppk_config in ipsec['authentication']['ppk'].items(): + if 'id' not in ppk_config: + raise ConfigError(f'Authentication PPK "{ppk}" missing "id"') + if 'secret' not in ppk_config: + raise ConfigError(f'Authentication PPK "{ppk}" missing "secret"') + for ppk_id in ppk_config['id']: + if ppk_id in ppk_ids: + raise ConfigError( + f'Authentication PPK "{ppk}" has duplicate ID "{ppk_id}" from another PPK. IDs should be unique.' + ) + ppk_ids.append(ppk_id) if 'interface' in ipsec: tmp = re.compile(dynamic_interface_pattern) @@ -445,6 +464,25 @@ def verify(ipsec): elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']: raise ConfigError(f'Requested pool "{pool}" does not exist!') + # T8136 IPSEC PPK Support + # PPKs and Childless only works with IKEv2. Check that ike-group is v2 if either option is enabled. Check that PPK ID was actually defined in authentication. Recommend use of childless when using PPKs if not already configured. + if 'ppk' in ra_conf['authentication']: + ike = ra_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError( + f'Incorrect configuration in IKE group "{ike}": post-quantum pre-shared keys require explicit IKEv2 usage.' + ) + if 'childless' not in ra_conf: + Warning( + 'It is recommended to use childless IKE SAs when using PPKs' + ) + if 'childless' in ra_conf: + ike = ra_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError( + f'Incorrect configuration in IKE group "{ike}": childless IKE SAs can only be used with IKEv2.' + ) + if 'pool' in ipsec['remote_access']: pool_networks = [] for pool, pool_config in ipsec['remote_access']['pool'].items(): @@ -653,6 +691,25 @@ def verify(ipsec): f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP' ) + # T8136 IPSEC PPK Support + # PPKs and Childless only works with IKEv2. Check that ike-group is v2 if either option is enabled. Check that PPK ID was actually defined in authentication. Recommend use of childless when using PPKs if not already configured. + if 'ppk' in peer_conf['authentication']: + ike = peer_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError( + f'Post-quantum preshared keys must be used with IKEv2! Please configure IKEv2 key-exchange in ike-group "{ike}".' + ) + if 'childless' not in peer_conf: + Warning( + 'It is recommended to use childless IKE SAs when using PPKs' + ) + if 'childless' in peer_conf: + ike = peer_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError( + f'Childless IKE SAs be used with IKEv2! Please configure IKEv2 key-exchange in ike-group "{ike}".' + ) + def cleanup_pki_files(): for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]: diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 4ff035416..9e45f3265 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -229,6 +229,28 @@ def _get_parent_sa_state(connection_name: str, data: list) -> str: ike_state = 'up' return ike_state +def _get_parent_ppk_state(connection_name: str, data: list) -> str: + """Get paren PPK state by connection name + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + Parent PPK state + """ + ppk_state = 'no' + if not data: + return ppk_state + for sa in data: + # check if parent PPK exists + for connection, connection_conf in sa.items(): + if connection_name != connection: + continue + if 'ppk' in connection_conf and connection_conf['ppk'].lower() == 'yes': + ppk_state = 'yes' + return ppk_state + def _get_child_sa_state( connection_name: str, tunnel_name: str, data: list, mode: str @@ -335,6 +357,17 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: base_list['local_id'] = conn_conf.get('local-1', '').get('id') base_list['remote_id'] = conn_conf.get('remote-1', '').get('id') base_list['version'] = conn_conf.get('version', 'IKE') + if conn_conf.get('ppk_id'): + if conn_conf.get('ppk_required') == 'yes': + base_list['ppk'] = 'req/' + _get_parent_ppk_state( + connection, list_sas + ) + else: + base_list['ppk'] = 'opt/' + _get_parent_ppk_state( + connection, list_sas + ) + else: + base_list['ppk'] = 'none/' + _get_parent_ppk_state(connection, list_sas) base_list['children'] = [] children = conn_conf['children'] for tunnel, tun_options in children.items(): @@ -400,6 +433,8 @@ def _get_formatted_output_conections(data): f'{entry["ike_proposal"]["hash"]}/' f'{entry["ike_proposal"]["dh"]}' ) + ppk = entry['ppk'] + connections.append( [ ike_name, @@ -411,6 +446,7 @@ def _get_formatted_output_conections(data): local_id, remote_id, proposal, + ppk, ] ) for tun in entry['children']: @@ -428,6 +464,7 @@ def _get_formatted_output_conections(data): f'{tun["esp_proposal"]["hash"]}/' f'{tun["esp_proposal"]["dh"]}' ) + ppk = '-' connections.append( [ tun_name, @@ -439,6 +476,7 @@ def _get_formatted_output_conections(data): local_id, remote_id, proposal, + ppk, ] ) connection_headers = [ @@ -451,8 +489,12 @@ def _get_formatted_output_conections(data): 'Local id', 'Remote id', 'Proposal', + 'PPK', ] - output = tabulate(connections, connection_headers, numalign='left') + output = ( + 'PPK Codes: none - Not Configured, opt - PPK is Optional, req - PPK is required, no - PPK not negotiated, yes - PPK negotiated\n' + + tabulate(connections, connection_headers, numalign='left') + ) return output |
