From 865cba395223afaa263ff7581fb53ee509f36266 Mon Sep 17 00:00:00 2001 From: Eshenko Dmitriy Date: Wed, 19 Aug 2020 00:37:00 +0300 Subject: anyconnect: T2036: add anyconnect VPN support --- data/templates/ocserv/ocserv_config.tmpl | 82 +++++++ data/templates/ocserv/ocserv_passwd.tmpl | 6 + data/templates/ocserv/radius_conf.tmpl | 22 ++ data/templates/ocserv/radius_servers.tmpl | 7 + debian/control | 1 + interface-definitions/vpn_anyconnect.xml.in | 258 +++++++++++++++++++++ op-mode-definitions/anyconnect.xml | 20 ++ src/conf_mode/vpn_anyconnect.py | 132 +++++++++++ .../systemd/system/ocserv.service.d/override.conf | 14 ++ src/op_mode/anyconnect-control.py | 67 ++++++ 10 files changed, 609 insertions(+) create mode 100644 data/templates/ocserv/ocserv_config.tmpl create mode 100644 data/templates/ocserv/ocserv_passwd.tmpl create mode 100644 data/templates/ocserv/radius_conf.tmpl create mode 100644 data/templates/ocserv/radius_servers.tmpl create mode 100644 interface-definitions/vpn_anyconnect.xml.in create mode 100644 op-mode-definitions/anyconnect.xml create mode 100755 src/conf_mode/vpn_anyconnect.py create mode 100644 src/etc/systemd/system/ocserv.service.d/override.conf create mode 100755 src/op_mode/anyconnect-control.py diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl new file mode 100644 index 000000000..6aaeff693 --- /dev/null +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -0,0 +1,82 @@ +### generated by vpn_anyconnect.py ### + +tcp-port = {{ listen_ports.tcp }} +udp-port = {{ listen_ports.udp }} + +run-as-user = nobody +run-as-group = daemon + +{% if "radius" in authentication.mode %} +auth = "radius [config=/run/ocserv/radiusclient.conf]" +{% else %} +auth = "plain[/run/ocserv/ocpasswd]" +{% endif %} + +{% if ssl.cert_file %} +server-cert = {{ ssl.cert_file }} +{% endif %} + +{% if ssl.key_file %} +server-key = {{ ssl.key_file }} +{% endif %} + +{% if ssl.ca_cert_file %} +ca-cert = {{ ssl.ca_cert_file }} +{% endif %} + +socket-file = /run/ocserv/ocserv.socket +occtl-socket-file = /run/ocserv/occtl.socket +use-occtl = true +isolate-workers = true +keepalive = 300 +dpd = 60 +mobile-dpd = 300 +switch-to-tcp-timeout = 30 +tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128" +auth-timeout = 240 +idle-timeout = 1200 +mobile-idle-timeout = 1800 +min-reauth-time = 3 +cookie-timeout = 300 +rekey-method = ssl +try-mtu-discovery = true +cisco-client-compat = true +dtls-legacy = true + + +# The name to use for the tun device +device = sslvpn + +# An alternative way of specifying the network: +{% if network_settings %} +# DNS settings +{% if network_settings.name_server is string %} +dns = {{ network_settings.name_server }} +{% else %} +{% for dns in network_settings.name_server %} +dns = {{ dns }} +{% endfor %} +{% endif %} +# IPv4 network pool +{% if network_settings.client_ip_settings %} +{% if network_settings.client_ip_settings.subnet %} +ipv4-network = {{ network_settings.client_ip_settings.subnet }} +{% endif %} +{% endif %} +# IPv6 network pool +{% if network_settings.client_ipv6_pool %} +{% if network_settings.client_ipv6_pool.prefix %} +ipv6-network = {{ network_settings.client_ipv6_pool.prefix }} +ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }} +{% endif %} +{% endif %} +{% endif %} + +{% if network_settings.push_route is string %} +route = {{ network_settings.push_route }} +{% else %} +{% for route in network_settings.push_route %} +route = {{ route }} +{% endfor %} +{% endif %} + diff --git a/data/templates/ocserv/ocserv_passwd.tmpl b/data/templates/ocserv/ocserv_passwd.tmpl new file mode 100644 index 000000000..ffadb4860 --- /dev/null +++ b/data/templates/ocserv/ocserv_passwd.tmpl @@ -0,0 +1,6 @@ +#:: +{% for user in username if username is defined %} +{% if not "disable" in username[user] %} +{{ user }}:*:{{ username[user].hash }} +{% endif %} +{% endfor %} \ No newline at end of file diff --git a/data/templates/ocserv/radius_conf.tmpl b/data/templates/ocserv/radius_conf.tmpl new file mode 100644 index 000000000..2d19306a0 --- /dev/null +++ b/data/templates/ocserv/radius_conf.tmpl @@ -0,0 +1,22 @@ +### generated by cpn_anyconnect.py ### +nas-identifier VyOS +{% for srv in server %} +{% if not "disable" in server[srv] %} +{% if "port" in server[srv] %} +authserver {{ srv }}:{{server[srv]["port"]}} +{% else %} +authserver {{ srv }} +{% endif %} +{% endif %} +{% endfor %} +radius_timeout {{ timeout }} +{% if source_address %} +bindaddr {{ source_address }} +{% else %} +bindaddr * +{% endif %} +servers /run/ocserv/radius_servers +dictionary /etc/radcli/dictionary +default_realm +radius_retries 3 +# \ No newline at end of file diff --git a/data/templates/ocserv/radius_servers.tmpl b/data/templates/ocserv/radius_servers.tmpl new file mode 100644 index 000000000..ba21fa074 --- /dev/null +++ b/data/templates/ocserv/radius_servers.tmpl @@ -0,0 +1,7 @@ +### generated by cpn_anyconnect.py ### +# server key +{% for srv in server %} +{% if not "disable" in server[srv] %} +{{ srv }} {{ server[srv].key }} +{% endif %} +{% endfor %} diff --git a/debian/control b/debian/control index 34ba4be8e..520401d57 100644 --- a/debian/control +++ b/debian/control @@ -107,6 +107,7 @@ Depends: python3, libatomic1, fastnetmon, libndp-tools, + ocserv, tcptraceroute Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/interface-definitions/vpn_anyconnect.xml.in b/interface-definitions/vpn_anyconnect.xml.in new file mode 100644 index 000000000..e74326986 --- /dev/null +++ b/interface-definitions/vpn_anyconnect.xml.in @@ -0,0 +1,258 @@ + + + + + + + SSL VPN AnyConnect + 901 + + + + + Authentication for remote access SSL VPN Server + + + + + Authentication mode used by this server + + local + Use local username/password configuration + + + radius + Use RADIUS server for user autentication + + + (local|radius) + + + local radius + + + + + + Local user authentication for SSL VPN server + + + + + User name for authentication + + + + + Option to disable a SSL VPN Server user + + + + + + Password for authentication + + + + + + + #include + + + + + Session timeout + + 1-30 + Session timeout in seconds (default: 2) + + + + + Timeout must be between 1 and 30 seconds + + 2 + + + + + + + + SSL Certificate, SSL Key and CA (/config/auth) + + + + + tcp port number to accept connections (default: 443) + + 1-65535 + Numeric IP port (default: 443) + + + + + + 443 + + + + udp port number to accept connections (default: 443) + + 1-65535 + Numeric IP port (default: 443) + + + + + + 443 + + + + + + SSL Certificate, SSL Key and CA (/config/auth) + + + + + Certificate Authority certificate + + + + + file + File in /config/auth directory + + + + + + + + + Server Certificate + + file + File in /config/auth directory + + + + + + + + + Privat Key of the Server Certificate + + file + File in /config/auth directory + + + + + + + + + + + Network settings + + + + + Route to be pushed to the client + + ipv4net + IPv4 network and prefix length + + + ipv6net + IPv6 network and prefix length + + + + + + + + + + Client IP pools settings + + + + + Client IP subnet (CIDR notation) + + ipv4net + IPv4 address and prefix length + + + + + Not a valid CIDR formatted prefix + + + + + + + Pool of client IPv6 addresses + + + + + Pool of addresses used to assign to clients + + ipv6net + IPv6 address and prefix length + + + + + + + + + Prefix length used for individual client + + <48-128> + Client prefix length (default: 64) + + + + + + 64 + + + + + + Domain Name Server (DNS) propagated to client + + ipv4 + Domain Name Server (DNS) IPv4 address + + + ipv6 + Domain Name Server (DNS) IPv6 address + + + + + + + + + + + + + + + diff --git a/op-mode-definitions/anyconnect.xml b/op-mode-definitions/anyconnect.xml new file mode 100644 index 000000000..7e8cdd35b --- /dev/null +++ b/op-mode-definitions/anyconnect.xml @@ -0,0 +1,20 @@ + + + + + + + show anyconnect-server information + + + + + Show active anyconnect server sessions + + ${vyos_op_scripts_dir}/anyconnect-control.py --action="show_sessions" + + + + + + diff --git a/src/conf_mode/vpn_anyconnect.py b/src/conf_mode/vpn_anyconnect.py new file mode 100755 index 000000000..45c06bffa --- /dev/null +++ b/src/conf_mode/vpn_anyconnect.py @@ -0,0 +1,132 @@ +#!/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 os +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.xml import defaults +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from crypt import crypt, mksalt, METHOD_SHA512 + +from vyos import airbag +airbag.enable() + +cfg_dir = '/run/ocserv' +ocserv_conf = cfg_dir + '/ocserv.conf' +ocserv_passwd = cfg_dir + '/ocpasswd' +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 get_config(): + conf = Config() + base = ['vpn', 'anyconnect'] + ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + default_values = defaults(base) + ocserv = dict_merge(default_values, ocserv) + return ocserv + + +def verify(ocserv): + if ocserv is None: + return None + + # Check authentication + if "authentication" in ocserv: + if "mode" in ocserv["authentication"]: + if "local" in ocserv["authentication"]["mode"]: + if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('Anyconect mode local required at leat one user') + else: + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + raise ConfigError(f'password required for user {user}') + else: + raise ConfigError('anyconnect authentication mode required') + else: + raise ConfigError('anyconnect authentication credentials required') + + # Check ssl + if "ssl" in ocserv: + req_cert = ['ca_cert_file', 'cert_file', 'key_file'] + for cert in req_cert: + if not cert in ocserv["ssl"]: + raise ConfigError('anyconnect ssl {0} required'.format(cert.replace('_', '-'))) + else: + raise ConfigError('anyconnect ssl required') + + # 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('anyconnect network settings required') + + +def generate(ocserv): + if not ocserv: + return None + + if "radius" in ocserv["authentication"]["mode"]: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + 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.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + + # Render config + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) + + + +def apply(ocserv): + if not ocserv: + call('systemctl stop ocserv.service') + for file in [ocserv_conf, ocserv_passwd]: + if os.path.exists(file): + os.unlink(file) + else: + call('systemctl restart ocserv.service') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/etc/systemd/system/ocserv.service.d/override.conf b/src/etc/systemd/system/ocserv.service.d/override.conf new file mode 100644 index 000000000..89dbb153f --- /dev/null +++ b/src/etc/systemd/system/ocserv.service.d/override.conf @@ -0,0 +1,14 @@ +[Unit] +RequiresMountsFor=/run +ConditionPathExists=/run/ocserv/ocserv.conf +After= +After=vyos-router.service +After=dbus.service + +[Service] +WorkingDirectory=/run/ocserv +PIDFile= +PIDFile=/run/ocserv/ocserv.pid +ExecStart= +ExecStart=/usr/sbin/ocserv --foreground --pid-file /run/ocserv/ocserv.pid --config /run/ocserv/ocserv.conf + diff --git a/src/op_mode/anyconnect-control.py b/src/op_mode/anyconnect-control.py new file mode 100755 index 000000000..6382016b7 --- /dev/null +++ b/src/op_mode/anyconnect-control.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 sys +import argparse +import json + +from vyos.config import Config +from vyos.util import popen, run, DEVNULL +from tabulate import tabulate + +occtl = '/usr/bin/occtl' +occtl_socket = '/run/ocserv/occtl.socket' + +def show_sessions(): + out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL) + if code: + sys.exit('Cannot get anyconnect users information') + else: + headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"] + sessions = json.loads(out) + ses_list = [] + for ses in sessions: + ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]]) + if len(ses_list) > 0: + print(tabulate(ses_list, headers)) + else: + print("No active anyconnect sessions") + +def is_ocserv_configured(): + if not Config().exists_effective('vpn anyconnect'): + print("vpn anyconnect server is not configured") + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) + parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) + args = parser.parse_args() + + + # Check is IPoE configured + is_ocserv_configured() + + if args.action == "restart": + run("systemctl restart ocserv") + sys.exit(0) + elif args.action == "show_sessions": + show_sessions() + +if __name__ == '__main__': + main() -- cgit v1.2.3