summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/configd-include.json1
-rw-r--r--data/templates/ipsec/charon/dhcp.conf.tmpl23
-rw-r--r--data/templates/ipsec/swanctl.conf.tmpl14
-rw-r--r--data/templates/ipsec/swanctl/remote_access.tmpl16
-rw-r--r--interface-definitions/ipsec-settings.xml.in25
-rw-r--r--interface-definitions/vpn_ipsec.xml.in82
-rw-r--r--python/vyos/pki.py22
-rw-r--r--python/vyos/util.py15
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py7
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py96
-rwxr-xr-xsrc/op_mode/pki.py32
11 files changed, 267 insertions, 66 deletions
diff --git a/data/configd-include.json b/data/configd-include.json
index 2e6226097..d228ac8a3 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -27,7 +27,6 @@
"interfaces-wireguard.py",
"interfaces-wireless.py",
"interfaces-wwan.py",
-"ipsec-settings.py",
"lldp.py",
"nat.py",
"nat66.py",
diff --git a/data/templates/ipsec/charon/dhcp.conf.tmpl b/data/templates/ipsec/charon/dhcp.conf.tmpl
new file mode 100644
index 000000000..2879550a8
--- /dev/null
+++ b/data/templates/ipsec/charon/dhcp.conf.tmpl
@@ -0,0 +1,23 @@
+dhcp {
+ load = yes
+
+{% if options is defined and options.remote_access is defined and options.remote_access.dhcp_pool is defined %}
+{% if options.remote_access.dhcp_pool.interface is defined %}
+ interface = {{ options.remote_access.dhcp_pool.interface }}
+{% endif %}
+{% if options.remote_access.dhcp_pool.server is defined %}
+ server = {{ options.remote_access.dhcp_pool.server }}
+{% endif %}
+{% endif %}
+
+ # Always use the configured server address.
+ # force_server_address = no
+
+ # Derive user-defined MAC address from hash of IKE identity and send client
+ # identity DHCP option.
+ # identity_lease = no
+
+ # Use the DHCP server port (67) as source port when a unicast server address
+ # is configured.
+ # use_server_port = no
+}
diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl
index 0eda8479a..00251d44d 100644
--- a/data/templates/ipsec/swanctl.conf.tmpl
+++ b/data/templates/ipsec/swanctl.conf.tmpl
@@ -23,7 +23,7 @@ connections {
pools {
{% if remote_access is defined %}
-{% for ra, ra_conf in remote_access.items() if remote_access is defined %}
+{% for ra, ra_conf in remote_access.items() if ra_conf.pool.dhcp_enable is not defined %}
ra-{{ ra }} {
addrs = {{ ra_conf.pool.prefix }}
dns = {{ ra_conf.pool.name_server | join(",") }}
@@ -82,7 +82,17 @@ secrets {
{% endif %}
{% if remote_access is defined %}
{% for ra, ra_conf in remote_access.items() if remote_access is defined %}
-{% if ra_conf.authentication is defined and ra_conf.authentication.local_users is defined and ra_conf.authentication.local_users.username is defined %}
+{% if ra_conf.authentication.server_mode == 'pre-shared-secret' %}
+ ike_{{ ra }} {
+{% if ra_conf.authentication.id is defined %}
+ id = "{{ ra_conf.authentication.id }}"
+{% elif ra_conf.local_address is defined %}
+ id = "{{ ra_conf.local_address }}"
+{% endif %}
+ secret = "{{ ra_conf.authentication.pre_shared_secret }}"
+ }
+{% endif %}
+{% if ra_conf.authentication.client_mode == 'eap-mschapv2' and ra_conf.authentication.local_users is defined and ra_conf.authentication.local_users.username is defined %}
{% for user, user_conf in ra_conf.authentication.local_users.username.items() if user_conf.disable is not defined %}
eap-{{ ra }}-{{ user }} {
secret = "{{ user_conf.password }}"
diff --git a/data/templates/ipsec/swanctl/remote_access.tmpl b/data/templates/ipsec/swanctl/remote_access.tmpl
index a3a1cf0b2..95f2108fb 100644
--- a/data/templates/ipsec/swanctl/remote_access.tmpl
+++ b/data/templates/ipsec/swanctl/remote_access.tmpl
@@ -10,19 +10,27 @@
send_certreq = no
rekey_time = {{ ike.lifetime }}s
keyingtries = 0
+{% if rw_conf.pool.dhcp_enable is defined %}
+ pools = dhcp
+{% else %}
pools = ra-{{ name }}
+{% endif %}
local {
- auth = pubkey
-{% if rw_conf.authentication is defined and rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %}
+{% if rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %}
id = "{{ rw_conf.authentication.id }}"
{% endif %}
-{% if rw_conf.authentication is defined and rw_conf.authentication.x509 is defined and rw_conf.authentication.x509.certificate is defined %}
+{% if rw_conf.authentication.server_mode == 'x509' %}
+ auth = pubkey
certs = {{ rw_conf.authentication.x509.certificate }}.pem
+{% elif rw_conf.authentication.server_mode == 'pre-shared-secret' %}
+ auth = psk
{% endif %}
}
remote {
- auth = eap-mschapv2
+ auth = {{ rw_conf.authentication.client_mode }}
+{% if rw_conf.authentication.client_mode.startswith("eap") %}
eap_id = %any
+{% endif %}
}
children {
ikev2-vpn {
diff --git a/interface-definitions/ipsec-settings.xml.in b/interface-definitions/ipsec-settings.xml.in
deleted file mode 100644
index 0bcba9a84..000000000
--- a/interface-definitions/ipsec-settings.xml.in
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
- <node name="vpn">
- <children>
- <node name="ipsec">
- <children>
- <node name="options" owner="${vyos_conf_scripts_dir}/ipsec-settings.py">
- <properties>
- <help>Global IPsec settings</help>
- <priority>902</priority>
- </properties>
- <children>
- <leafNode name="disable-route-autoinstall">
- <properties>
- <valueless/>
- <help>Do not automatically install routes to remote networks</help>
- </properties>
- </leafNode>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index f6b18d1d5..4425ab02a 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -648,6 +648,37 @@
<valueless/>
</properties>
</leafNode>
+ <node name="remote-access">
+ <properties>
+ <help>remote-access global options</help>
+ </properties>
+ <children>
+ <node name="dhcp-pool">
+ <properties>
+ <help>DHCP pool options for remote-access</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface with DHCP server to use</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>DHCP server address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of the DHCP server</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
</children>
</node>
<tagNode name="profile">
@@ -720,6 +751,26 @@
<children>
#include <include/ipsec/authentication-id.xml.i>
#include <include/ipsec/authentication-x509.xml.i>
+ <leafNode name="client-mode">
+ <properties>
+ <help>Client authentication mode</help>
+ <completionHelp>
+ <list>eap-tls eap-mschapv2</list>
+ </completionHelp>
+ <valueHelp>
+ <format>eap-tls</format>
+ <description>EAP-TLS</description>
+ </valueHelp>
+ <valueHelp>
+ <format>eap-mschapv2</format>
+ <description>EAP-MSCHAPv2</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(eap-tls|eap-mschapv2)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>eap-mschapv2</defaultValue>
+ </leafNode>
<node name="local-users">
<properties>
<help>Local user authentication for PPPoE server</help>
@@ -740,6 +791,31 @@
</tagNode>
</children>
</node>
+ <leafNode name="server-mode">
+ <properties>
+ <help>Server authentication mode</help>
+ <completionHelp>
+ <list>pre-shared-secret x509</list>
+ </completionHelp>
+ <valueHelp>
+ <format>pre-shared-secret</format>
+ <description>pre-shared-secret_description</description>
+ </valueHelp>
+ <valueHelp>
+ <format>x509</format>
+ <description>x509_description</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(pre-shared-secret|x509)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>x509</defaultValue>
+ </leafNode>
+ <leafNode name="pre-shared-secret">
+ <properties>
+ <help>Pre-shared-secret used for server authentication</help>
+ </properties>
+ </leafNode>
</children>
</node>
#include <include/generic-description.xml.i>
@@ -753,6 +829,12 @@
<help>IP address pool for remote-access users</help>
</properties>
<children>
+ <leafNode name="dhcp-enable">
+ <properties>
+ <help>Enable DHCP pool for clients on this connection</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="exclude">
<properties>
<help>Local IPv4 or IPv6 pool prefix exclusions</help>
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index a575ac16a..1c6282d84 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
+import ipaddress
from cryptography import x509
from cryptography.exceptions import InvalidSignature
@@ -112,7 +113,7 @@ def create_private_key(key_type, key_size=None):
private_key = ec.generate_private_key(curve)
return private_key
-def create_certificate_request(subject, private_key):
+def create_certificate_request(subject, private_key, subject_alt_names=[]):
subject_obj = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, subject['country']),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, subject['state']),
@@ -120,9 +121,20 @@ def create_certificate_request(subject, private_key):
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject['organization']),
x509.NameAttribute(NameOID.COMMON_NAME, subject['common_name'])])
- return x509.CertificateSigningRequestBuilder() \
- .subject_name(subject_obj) \
- .sign(private_key, hashes.SHA256())
+ builder = x509.CertificateSigningRequestBuilder() \
+ .subject_name(subject_obj)
+
+ if subject_alt_names:
+ alt_names = []
+ for obj in subject_alt_names:
+ if isinstance(obj, ipaddress.IPv4Address) or isinstance(obj, ipaddress.IPv6Address):
+ alt_names.append(x509.IPAddress(obj))
+ elif isinstance(obj, str):
+ alt_names.append(x509.DNSName(obj))
+ if alt_names:
+ builder = builder.add_extension(x509.SubjectAlternativeName(alt_names), critical=False)
+
+ return builder.sign(private_key, hashes.SHA256())
def add_key_identifier(ca_cert):
try:
@@ -166,7 +178,7 @@ def create_certificate(cert_req, ca_cert, ca_private_key, valid_days=365, cert_t
builder = builder.add_extension(add_key_identifier(ca_cert), critical=False)
for ext in cert_req.extensions:
- builder = builder.add_extension(ext, critical=False)
+ builder = builder.add_extension(ext.value, critical=False)
return builder.sign(ca_private_key, hashes.SHA256())
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 8247ccb2d..171ab397f 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -440,7 +440,6 @@ def process_running(pid_file):
pid = f.read().strip()
return pid_exists(int(pid))
-
def process_named_running(name):
""" Checks if process with given name is running and returns its PID.
If Process is not running, return None
@@ -451,7 +450,6 @@ def process_named_running(name):
return p.pid
return None
-
def seconds_to_human(s, separator=""):
""" Converts number of seconds passed to a human-readable
interval such as 1w4d18h35m59s
@@ -705,6 +703,19 @@ def dict_search(path, my_dict):
c = c.get(p, {})
return c.get(parts[-1], None)
+def dict_search_args(dict_object, *path):
+ # Traverse dictionary using variable arguments
+ # Added due to above function not allowing for '.' in the key names
+ # Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...)
+ if not isinstance(dict_object, dict) or not path:
+ return None
+
+ for item in path:
+ if item not in dict_object:
+ return None
+ dict_object = dict_object[item]
+ return dict_object
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index a65e8b567..a373f821f 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -29,7 +29,6 @@ from vyos import airbag
airbag.enable()
ra_conn_name = "remote-access"
-charon_conf_file = "/etc/strongswan.d/charon.conf"
ipsec_secrets_file = "/etc/ipsec.secrets"
ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
@@ -46,10 +45,6 @@ def get_config(config=None):
config = config
else:
config = Config()
- data = {"install_routes": "yes"}
-
- if config.exists("vpn ipsec options disable-route-autoinstall"):
- data["install_routes"] = "no"
if config.exists("vpn ipsec ipsec-interfaces interface"):
data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface")
@@ -170,8 +165,6 @@ def verify(data):
raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")
def generate(data):
- render(charon_conf_file, 'ipsec/charon.tmpl', data)
-
if data["ipsec_l2tp"]:
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
# old_umask = os.umask(0o077)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 50223320d..53a50fa1e 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -19,6 +19,7 @@ import os
from sys import exit
from time import sleep
+from time import time
from vyos.config import Config
from vyos.configdict import leaf_node_changed
@@ -33,7 +34,7 @@ from vyos.template import ip_from_cidr
from vyos.template import render
from vyos.validate import is_ipv6_link_local
from vyos.util import call
-from vyos.util import dict_search
+from vyos.util import dict_search_args
from vyos.util import run
from vyos.xml import defaults
from vyos import ConfigError
@@ -46,9 +47,15 @@ dhcp_wait_sleep = 1
swanctl_dir = '/etc/swanctl'
ipsec_conf = '/etc/ipsec.conf'
ipsec_secrets = '/etc/ipsec.secrets'
+charon_conf = '/etc/strongswan.d/charon.conf'
+charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
interface_conf = '/etc/strongswan.d/interfaces_use.conf'
swanctl_conf = f'{swanctl_dir}/swanctl.conf'
+default_install_routes = 'yes'
+
+vici_socket = '/var/run/charon.vici'
+
CERT_PATH = f'{swanctl_dir}/x509/'
KEY_PATH = f'{swanctl_dir}/private/'
CA_PATH = f'{swanctl_dir}/x509ca/'
@@ -100,6 +107,7 @@ def get_config(config=None):
ipsec['remote_access'][rw])
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 + ['ipsec-interfaces',
'interface'])
ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access',
@@ -116,7 +124,7 @@ def get_config(config=None):
return ipsec
def get_rsa_local_key(ipsec):
- return dict_search('local_key.file', ipsec['rsa_keys'])
+ return dict_search_args(ipsec['rsa_keys'], 'local_key', 'file')
def verify_rsa_local_key(ipsec):
file = get_rsa_local_key(ipsec)
@@ -132,7 +140,7 @@ def verify_rsa_local_key(ipsec):
return False
def verify_rsa_key(ipsec, key_name):
- return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys'])
+ return dict_search_args(ipsec['rsa_keys'], 'rsa_key_name', key_name, 'rsa_key')
def get_dhcp_address(iface):
addresses = Interface(iface).get_addr()
@@ -150,13 +158,13 @@ def verify_pki(pki, x509_conf):
ca_cert_name = x509_conf['ca_certificate']
cert_name = x509_conf['certificate']
- if not dict_search(f'ca.{ca_cert_name}.certificate', ipsec['pki']):
+ if not dict_search_args(pki, 'ca', ca_cert_name, 'certificate'):
raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"')
- if not dict_search(f'certificate.{cert_name}.certificate', ipsec['pki']):
+ if not dict_search_args(pki, 'certificate', cert_name, 'certificate'):
raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"')
- if not dict_search(f'certificate.{cert_name}.private.key', ipsec['pki']):
+ if not dict_search_args(pki, 'certificate', cert_name, 'private', 'key'):
raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"')
return True
@@ -190,6 +198,37 @@ def verify(ipsec):
if 'authentication' not in profile_conf:
raise ConfigError(f"Missing authentication on {profile} profile")
+ if 'remote_access' in ipsec:
+ for name, ra_conf in ipsec['remote_access'].items():
+ if 'esp_group' in ra_conf:
+ if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on {name} remote-access config")
+ else:
+ raise ConfigError(f"Missing esp-group on {name} remote-access config")
+
+ if 'ike_group' in ra_conf:
+ if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on {name} remote-access config")
+ else:
+ raise ConfigError(f"Missing ike-group on {name} remote-access config")
+
+ if 'authentication' not in ra_conf:
+ raise ConfigError(f"Missing authentication on {name} remote-access config")
+
+ if ra_conf['authentication']['server_mode'] == 'x509':
+ if 'x509' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing x509 settings on {name} remote-access config")
+
+ x509 = ra_conf['authentication']['x509']
+
+ if 'ca_certificate' not in x509 or 'certificate' not in x509:
+ raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
+
+ verify_pki(ipsec['pki'], x509)
+ elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
+ if 'pre_shared_secret' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+
if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
for peer, peer_conf in ipsec['site_to_site']['peer'].items():
has_default_esp = False
@@ -282,15 +321,24 @@ def verify(ipsec):
if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+def cleanup_pki_files():
+ for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH]:
+ if not os.path.exists(path):
+ continue
+ for file in os.listdir(path):
+ file_path = os.path.join(path, file)
+ if os.path.isfile(file_path):
+ os.unlink(file_path)
+
def generate_pki_files(pki, x509_conf):
ca_cert_name = x509_conf['ca_certificate']
- ca_cert_data = dict_search(f'ca.{ca_cert_name}.certificate', pki)
- ca_cert_crls = dict_search(f'ca.{ca_cert_name}.crl', pki) or []
+ ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate')
+ ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or []
crl_index = 1
cert_name = x509_conf['certificate']
- cert_data = dict_search(f'certificate.{cert_name}.certificate', pki)
- key_data = dict_search(f'certificate.{cert_name}.private.key', pki)
+ cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate')
+ key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key')
protected = 'passphrase' in x509_conf
with open(os.path.join(CA_PATH, f'{ca_cert_name}.pem'), 'w') as f:
@@ -308,10 +356,13 @@ def generate_pki_files(pki, x509_conf):
f.write(wrap_private_key(key_data, protected))
def generate(ipsec):
+ cleanup_pki_files()
+
if not ipsec:
- for config_file in [ipsec_conf, ipsec_secrets, interface_conf, swanctl_conf]:
+ for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
+ render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes})
return
if ipsec['dhcp_no_address']:
@@ -328,7 +379,7 @@ def generate(ipsec):
if not os.path.exists(KEY_PATH):
os.mkdir(KEY_PATH, mode=0o700)
- if 'remote_access' in ipsec:
+ if 'remote_access' in data:
for rw, rw_conf in ipsec['remote_access'].items():
if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']:
generate_pki_files(ipsec['pki'], rw_conf['authentication']['x509'])
@@ -351,8 +402,8 @@ def generate(ipsec):
if 'tunnel' in peer_conf:
for tunnel, tunnel_conf in peer_conf['tunnel'].items():
- local_prefixes = dict_search('local.prefix', tunnel_conf)
- remote_prefixes = dict_search('remote.prefix', tunnel_conf)
+ local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix')
+ remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix')
if not local_prefixes or not remote_prefixes:
continue
@@ -371,6 +422,8 @@ def generate(ipsec):
render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', data)
render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', data)
+ render(charon_conf, 'ipsec/charon.tmpl', data)
+ render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', data)
render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', data)
render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', data)
@@ -390,6 +443,17 @@ def resync_nhrp(ipsec):
if tmp > 0:
print('ERROR: failed to reapply NHRP settings!')
+def wait_for_vici_socket(timeout=5, sleep_interval=0.1):
+ start_time = time()
+ test_command = f'sudo socat -u OPEN:/dev/null UNIX-CONNECT:{vici_socket}'
+ while True:
+ if (start_time + timeout) < time():
+ return None
+ result = run(test_command)
+ if result == 0:
+ return True
+ sleep(sleep_interval)
+
def apply(ipsec):
if not ipsec:
call('sudo ipsec stop')
@@ -401,8 +465,8 @@ def apply(ipsec):
call('sudo ipsec rereadall')
call('sudo ipsec reload')
- sleep(5) # Give charon enough time to start
- call('sudo swanctl -q')
+ if wait_for_vici_socket():
+ call('sudo swanctl -q')
resync_l2tp(ipsec)
resync_nhrp(ipsec)
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index d7bb0d6ae..7dbeb4097 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
+import ipaddress
import os
import re
import sys
@@ -248,7 +249,24 @@ def generate_private_key():
return create_private_key(key_type, size), key_type
-def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False):
+def parse_san_string(san_string):
+ if not san_string:
+ return None
+
+ output = []
+ san_split = san_string.strip().split(",")
+
+ for pair_str in san_split:
+ tag, value = pair_str.strip().split(":", 1)
+ if tag == 'ipv4':
+ output.append(ipaddress.IPv4Address(value))
+ elif tag == 'ipv6':
+ output.append(ipaddress.IPv6Address(value))
+ elif tag == 'dns':
+ output.append(value)
+ return output
+
+def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, ask_san=True):
if not private_key:
private_key, key_type = generate_private_key()
@@ -259,8 +277,14 @@ def generate_certificate_request(private_key=None, key_type=None, return_request
subject['locality'] = ask_input('Enter locality:', default=default_values['locality'])
subject['organization'] = ask_input('Enter organization name:', default=default_values['organization'])
subject['common_name'] = ask_input('Enter common name:', default='vyos.io')
+ subject_alt_names = None
- cert_req = create_certificate_request(subject, private_key)
+ if ask_san and ask_yes_no('Do you want to configure Subject Alternative Names?'):
+ print("Enter alternative names in a comma separate list, example: ipv4:1.1.1.1,ipv6:fe80::1,dns:vyos.net")
+ san_string = ask_input('Enter Subject Alternative Names:')
+ subject_alt_names = parse_san_string(san_string)
+
+ cert_req = create_certificate_request(subject, private_key, subject_alt_names)
if return_request:
return cert_req
@@ -285,7 +309,7 @@ def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_
def generate_ca_certificate(name, install=False):
private_key, key_type = generate_private_key()
- cert_req = generate_certificate_request(private_key, key_type, return_request=True)
+ cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False)
cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True)
passphrase = ask_passphrase()
@@ -325,7 +349,7 @@ def generate_ca_certificate_sign(name, ca_name, install=False):
cert_req = None
if not ask_yes_no('Do you already have a certificate request?'):
private_key, key_type = generate_private_key()
- cert_req = generate_certificate_request(private_key, key_type, return_request=True)
+ cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False)
else:
print("Paste certificate request and press enter:")
lines = []