Page MenuHomeVyOS Platform

vpn_openconnect.py
No OneTemporary

Size
16 KB
Referenced Files
None
Subscribers
None

vpn_openconnect.py

#!/usr/bin/env python3
#
# Copyright (C) 2018-2023 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 <http://www.gnu.org/licenses/>.
import os
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import check_port_availability
from vyos.util import is_systemd_service_running
from vyos.util import is_listen_port_bind_service
from vyos.util import dict_search
from vyos.xml import defaults
from vyos import ConfigError
from crypt import crypt, mksalt, METHOD_SHA512
from time import sleep
from vyos import airbag
airbag.enable()
cfg_dir = '/run/ocserv'
ocserv_conf = cfg_dir + '/ocserv.conf'
ocserv_passwd = cfg_dir + '/ocpasswd'
ocserv_otp_usr = cfg_dir + '/users.oath'
radius_cfg = cfg_dir + '/radiusclient.conf'
radius_servers = cfg_dir + '/radius_servers'
# Generate hash from user cleartext password
def get_hash(password):
return crypt(password, mksalt(METHOD_SHA512))
def _default_dict_cleanup(origin: dict, default_values: dict) -> dict:
"""
https://vyos.dev/T2665
Clear unnecessary key values in merged config by dict_merge function
:param origin: config
:type origin: dict
:param default_values: default values
:type default_values: dict
:return: merged dict
:rtype: dict
"""
if 'mode' in origin["authentication"] and "local" in \
origin["authentication"]["mode"]:
del origin['authentication']['local_users']['username']['otp']
if not origin["authentication"]["local_users"]["username"]:
raise ConfigError(
'Openconnect authentication mode local requires at least one user')
default_ocserv_usr_values = \
default_values['authentication']['local_users']['username']['otp']
for user, params in origin['authentication']['local_users'][
'username'].items():
# Not every configuration requires OTP settings
if origin['authentication']['local_users']['username'][user].get(
'otp'):
origin['authentication']['local_users']['username'][user][
'otp'] = dict_merge(default_ocserv_usr_values,
origin['authentication'][
'local_users']['username'][user][
'otp'])
if 'mode' in origin["authentication"] and "radius" in \
origin["authentication"]["mode"]:
del origin['authentication']['radius']['server']['port']
if not origin["authentication"]['radius']['server']:
raise ConfigError(
'Openconnect authentication mode radius requires at least one RADIUS server')
default_values_radius_port = \
default_values['authentication']['radius']['server']['port']
for server, params in origin['authentication']['radius'][
'server'].items():
if 'port' not in params:
params['port'] = default_values_radius_port
if 'mode' in origin["accounting"] and "radius" in \
origin["accounting"]["mode"]:
del origin['accounting']['radius']['server']['port']
if not origin["accounting"]['radius']['server']:
raise ConfigError(
'Openconnect accounting mode radius requires at least one RADIUS server')
default_values_radius_port = \
default_values['accounting']['radius']['server']['port']
for server, params in origin['accounting']['radius'][
'server'].items():
if 'port' not in params:
params['port'] = default_values_radius_port
return origin
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
base = ['vpn', 'openconnect']
if not conf.exists(base):
return None
ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
# workaround a "know limitation" - https://vyos.dev/T2665
ocserv = _default_dict_cleanup(ocserv, default_values)
if ocserv:
ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
return ocserv
def verify(ocserv):
if ocserv is None:
return None
# Check if listen-ports not binded other services
# It can be only listen by 'ocserv-main'
for proto, port in ocserv.get('listen_ports').items():
if check_port_availability(ocserv['listen_address'], int(port), proto) is not True and \
not is_listen_port_bind_service(int(port), 'ocserv-main'):
raise ConfigError(f'"{proto}" port "{port}" is used by another service')
# Check accounting
if "accounting" in ocserv:
if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]:
if "authentication" not in ocserv or "mode" not in ocserv["authentication"]:
raise ConfigError('Accounting depends on OpenConnect authentication configuration')
elif "radius" not in ocserv["authentication"]["mode"]:
raise ConfigError('RADIUS accounting must be used with RADIUS authentication')
# Check authentication
if "authentication" in ocserv:
if "mode" in ocserv["authentication"]:
if "local" in ocserv["authentication"]["mode"]:
if "radius" in ocserv["authentication"]["mode"]:
raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration')
if not ocserv["authentication"]["local_users"]:
raise ConfigError('openconnect mode local required at least one user')
if not ocserv["authentication"]["local_users"]["username"]:
raise ConfigError('openconnect mode local required at least one user')
else:
# For OTP mode: verify that each local user has an OTP key
if "otp" in ocserv["authentication"]["mode"]["local"]:
users_wo_key = []
for user, user_config in ocserv["authentication"]["local_users"]["username"].items():
# User has no OTP key defined
if dict_search('otp.key', user_config) == None:
users_wo_key.append(user)
if users_wo_key:
raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}')
# For password (and default) mode: verify that each local user has password
if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]:
users_wo_pswd = []
for user in ocserv["authentication"]["local_users"]["username"]:
if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
users_wo_pswd.append(user)
if users_wo_pswd:
raise ConfigError(f'password required for users:\n{users_wo_pswd}')
# Validate that if identity-based-config is configured all child config nodes are set
if 'identity_based_config' in ocserv["authentication"]:
if 'disabled' not in ocserv["authentication"]["identity_based_config"]:
if 'mode' not in ocserv["authentication"]["identity_based_config"]:
raise ConfigError('OpenConnect radius identity-based-config enabled but mode not selected')
elif 'group' in ocserv["authentication"]["identity_based_config"]["mode"] and "radius" not in ocserv["authentication"]["mode"]:
raise ConfigError('OpenConnect config-per-group must be used with radius authentication')
if 'directory' not in ocserv["authentication"]["identity_based_config"]:
raise ConfigError('OpenConnect identity-based-config enabled but directory not set')
if 'default_config' not in ocserv["authentication"]["identity_based_config"]:
raise ConfigError('OpenConnect identity-based-config enabled but default-config not set')
else:
raise ConfigError('openconnect authentication mode required')
else:
raise ConfigError('openconnect authentication credentials required')
# Check ssl
if 'ssl' not in ocserv:
raise ConfigError('openconnect ssl required')
if not ocserv['pki'] or 'certificate' not in ocserv['pki']:
raise ConfigError('PKI not configured')
ssl = ocserv['ssl']
if 'certificate' not in ssl:
raise ConfigError('openconnect ssl certificate required')
cert_name = ssl['certificate']
if cert_name not in ocserv['pki']['certificate']:
raise ConfigError('Invalid openconnect ssl certificate')
cert = ocserv['pki']['certificate'][cert_name]
if 'certificate' not in cert:
raise ConfigError('Missing certificate in PKI')
if 'private' not in cert or 'key' not in cert['private']:
raise ConfigError('Missing private key in PKI')
if 'ca_certificate' in ssl:
if 'ca' not in ocserv['pki']:
raise ConfigError('PKI not configured')
if ssl['ca_certificate'] not in ocserv['pki']['ca']:
raise ConfigError('Invalid openconnect ssl CA certificate')
# Check network settings
if "network_settings" in ocserv:
if "push_route" in ocserv["network_settings"]:
# Replace default route
if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]:
ocserv["network_settings"]["push_route"].remove("0.0.0.0/0")
ocserv["network_settings"]["push_route"].append("default")
else:
ocserv["network_settings"]["push_route"] = ["default"]
else:
raise ConfigError('openconnect network settings required')
def generate(ocserv):
if not ocserv:
return None
if "radius" in ocserv["authentication"]["mode"]:
if dict_search(ocserv, 'accounting.mode.radius'):
# Render radius client configuration
render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"]
# Render radius servers
# Merge the accounting and authentication servers into a single dictionary
render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers})
else:
# Render radius client configuration
render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
# Render radius servers
render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"])
elif "local" in ocserv["authentication"]["mode"]:
# if mode "OTP", generate OTP users file parameters
if "otp" in ocserv["authentication"]["mode"]["local"]:
if "local_users" in ocserv["authentication"]:
for user in ocserv["authentication"]["local_users"]["username"]:
# OTP token type from CLI parameters:
otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval"))
token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type")
otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length"))
if token_type == "hotp-time":
otp_type = "HOTP/T" + otp_interval
elif token_type == "hotp-event":
otp_type = "HOTP/E"
else:
otp_type = "HOTP/T" + otp_interval
ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length
# if there is a password, generate hash
if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]:
if "local_users" in ocserv["authentication"]:
for user in ocserv["authentication"]["local_users"]["username"]:
ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
if "password-otp" in ocserv["authentication"]["mode"]["local"]:
# Render local users ocpasswd
render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
# Render local users OTP keys
render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
elif "password" in ocserv["authentication"]["mode"]["local"]:
# Render local users ocpasswd
render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
elif "otp" in ocserv["authentication"]["mode"]["local"]:
# Render local users OTP keys
render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
else:
# Render local users ocpasswd
render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
else:
if "local_users" in ocserv["authentication"]:
for user in ocserv["authentication"]["local_users"]["username"]:
ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
# Render local users
render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
if "ssl" in ocserv:
cert_file_path = os.path.join(cfg_dir, 'cert.pem')
cert_key_path = os.path.join(cfg_dir, 'cert.key')
ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem')
if 'certificate' in ocserv['ssl']:
cert_name = ocserv['ssl']['certificate']
pki_cert = ocserv['pki']['certificate'][cert_name]
with open(cert_file_path, 'w') as f:
f.write(wrap_certificate(pki_cert['certificate']))
if 'private' in pki_cert and 'key' in pki_cert['private']:
with open(cert_key_path, 'w') as f:
f.write(wrap_private_key(pki_cert['private']['key']))
if 'ca_certificate' in ocserv['ssl']:
ca_name = ocserv['ssl']['ca_certificate']
pki_ca_cert = ocserv['pki']['ca'][ca_name]
with open(ca_cert_file_path, 'w') as f:
f.write(wrap_certificate(pki_ca_cert['certificate']))
# Render config
render(ocserv_conf, 'ocserv/ocserv_config.j2', ocserv)
def apply(ocserv):
if not ocserv:
call('systemctl stop ocserv.service')
for file in [ocserv_conf, ocserv_passwd, ocserv_otp_usr]:
if os.path.exists(file):
os.unlink(file)
else:
call('systemctl reload-or-restart ocserv.service')
counter = 0
while True:
# exit early when service runs
if is_systemd_service_running("ocserv.service"):
break
sleep(0.250)
if counter > 5:
raise ConfigError('openconnect failed to start, check the logs for details')
break
counter += 1
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)

File Metadata

Mime Type
text/x-script.python
Expires
Sat, Oct 17, 12:52 AM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4380793
Default Alt Text
vpn_openconnect.py (16 KB)

Event Timeline