#!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import os from time import sleep # Top level import so that configd can override it from sys import argv from vyos.config import Config from vyos import ConfigError from vyos.util import call, wait_for_file_write_complete from vyos.template import render 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 ipsec_conf_file = "/etc/ipsec.conf" ca_cert_path = "/etc/ipsec.d/cacerts" server_cert_path = "/etc/ipsec.d/certs" server_key_path = "/etc/ipsec.d/private" delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" charon_pidfile = "/var/run/charon.pid" def get_config(config=None): if config: config = config else: config = Config() # IPsec isn't configured enough to warrant starting StrongSWAN for it, # it's just some incomplete or leftover options. if not (config.exists("vpn ipsec site-to-site peer") or \ config.exists("vpn ipsec profile") or \ config.exists("vpn l2tp remote-access ipsec-settings")): return {} 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") # Init config variables data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end data["ipsec_ra_conn_file"] = ipsec_ra_conn_file data["ra_conn_name"] = ra_conn_name # Get l2tp ipsec settings data["ipsec_l2tp"] = False conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful if config.exists(conf_ipsec_command): data["ipsec_l2tp"] = True # Authentication params if config.exists(conf_ipsec_command + "authentication mode"): data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode") if config.exists(conf_ipsec_command + "authentication pre-shared-secret"): data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret") # mode x509 if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"): data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file") if config.exists(conf_ipsec_command + "authentication x509 crl-file"): data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file") if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"): data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file") data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0) if config.exists(conf_ipsec_command + "authentication x509 server-key-file"): data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file") data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0) if config.exists(conf_ipsec_command + "authentication x509 server-key-password"): data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password") # Common l2tp ipsec params if config.exists(conf_ipsec_command + "ike-lifetime"): data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime") else: data["ipsec_l2tp_ike_lifetime"] = "3600" if config.exists(conf_ipsec_command + "lifetime"): data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime") else: data["ipsec_l2tp_lifetime"] = "3600" if config.exists("vpn l2tp remote-access outside-address"): data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address') return data def write_ipsec_secrets(c): if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end) elif c.get("ipsec_l2tp_auth_mode") == "x509": secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end) old_umask = os.umask(0o077) with open(ipsec_secrets_file, 'a+') as f: f.write(secret_txt) os.umask(old_umask) def write_ipsec_conf(c): ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) old_umask = os.umask(0o077) with open(ipsec_conf_file, 'a+') as f: f.write(ipsec_confg_txt) os.umask(old_umask) ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) ### Checking certificate storage and notice if certificate not in /config directory def check_cert_file_store(cert_name, file_path, dts_path): if not re.search('^\/config\/.+', file_path): print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.") #Checking file existence if not os.path.isfile(file_path): raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"") else: ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ # todo make check ret = call('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) def verify(data): if not data: return # l2tp ipsec check if data["ipsec_l2tp"]: # Checking dependecies for "authentication mode pre-shared-secret" if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": if not data.get("ipsec_l2tp_secret"): raise ConfigError("pre-shared-secret required") if not data.get("outside_addr"): raise ConfigError("outside-address not defined") # Checking dependecies for "authentication mode x509" if data.get("ipsec_l2tp_auth_mode") == "x509": if not data.get("ipsec_l2tp_x509_server_key_file"): raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.") else: check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path) if not data.get("ipsec_l2tp_x509_server_cert_file"): raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.") else: check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path) if not data.get("ipsec_l2tp_x509_ca_cert_file"): raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509") else: check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path) if not data.get('ipsec_interfaces'): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): if not data: return 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) # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_secrets(data) old_umask = os.umask(0o077) # Create tunnels directory if does not exist if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data) os.umask(old_umask) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) # old_umask = os.umask(0o077) # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_conf(data) else: if os.path.exists(ipsec_ra_conn_file): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) def is_charon_responsive(): # Check if charon responds to strokes # # Sometimes it takes time to fully initialize, # so waiting for the process to come to live isn't always enough # # There's no official "no-op" stroke so we use the "memusage" stroke as a substitute from os import system res = system("ipsec stroke memusage >&/dev/null") if res == 0: return True else: return False def restart_ipsec(): try: # Restart the IPsec daemon when it's running. # Since it's started by the legacy ipsec.pl in VyOS 1.3, # there's a chance that this script will run before charon is up, # so we can't assume it's running and have to check and wait if needed. # But before everything else, there's a catch! # This script is run from _two_ places: "vpn ipsec options" and the top level "vpn" node # When IPsec isn't set up yet, and a user wants to commit an IPsec config with some # "vpn ipsec settings", this script will first be called before StrongSWAN is started by vpn-config.pl! # Thus if this script is run from "settings" _and_ charon is unresponsive, # we shouldn't wait for it, else there will be a deadlock. # We indicate that by running the script under vyshim from "vpn ipsec options" (which sets a variable named "argv") # and running it without configd from "vpn ipsec" if "from-options" in argv: if not is_charon_responsive(): return # If we got this far, then we actually need to restart StrongSWAN # First, wait for charon to get started by the old vpn-config.pl script. from time import sleep, time from os import system now = time() while True: if (time() - now) > 60: raise OSError("Timeout waiting for the IPsec process to become responsive") if is_charon_responsive(): break sleep(5) # Force configuration load call('swanctl -q >&/dev/null') except OSError: raise ConfigError('VPN configuration error: IPSec process did not start.') def apply(data): if data: restart_ipsec() else: print("Note: the IPsec process will not start until you configure some tunnels, profiles, or L2TP/IPsec settings") if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)