Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F118309862
vpn_openconnect.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
16 KB
Referenced Files
None
Subscribers
None
vpn_openconnect.py
View Options
#!/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
Details
Attached
Mime Type
text/x-script.python
Expires
Sat, Oct 17, 12:52 AM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4380793
Default Alt Text
vpn_openconnect.py (16 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment