From 30e38b0cc393ebc9ca50e32c69793df9654367bf Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Sun, 19 Mar 2023 15:52:09 +0100 Subject: [PATCH] VPN: OpenVPN: Connection Status - refactor to MVC PR: https://github.com/opnsense/core/issues/6382 (cherry picked from commit b9a1633a18e702ce0908c87504d196b53e0f3dcd) (cherry picked from commit fa30a8c1e4ec76e4fe157239632b110f0c6798d1) (cherry picked from commit 16492cedddb2676b318ea3ed2909e4c6a2dcc09a) (cherry picked from commit b50e529511fa5065556ac99fa062c9a69af39bdd) (cherry picked from commit c8970545a77e1dfc3e6dac4f9d31c4551cace1f4) --- plist | 5 +- .../OpenVPN/Api/ServiceController.php | 221 +++++++++++++ .../OPNsense/OpenVPN/StatusController.php | 47 +++ .../mvc/app/models/OPNsense/Core/ACL/ACL.xml | 3 +- .../app/models/OPNsense/Core/Menu/Menu.xml | 2 +- .../app/views/OPNsense/OpenVPN/status.volt | 161 ++++++++++ src/opnsense/scripts/openvpn/kill_session.py | 82 +++++ src/opnsense/scripts/openvpn/ovpn_status.py | 2 +- .../conf/actions.d/actions_openvpn.conf | 6 + src/www/status_openvpn.php | 304 ------------------ src/www/widgets/include/openvpn.inc | 2 +- src/www/widgets/widgets/openvpn.widget.php | 9 +- 12 files changed, 530 insertions(+), 314 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/StatusController.php create mode 100644 src/opnsense/mvc/app/views/OPNsense/OpenVPN/status.volt create mode 100755 src/opnsense/scripts/openvpn/kill_session.py delete mode 100644 src/www/status_openvpn.php diff --git a/plist b/plist index 225b0d2feed..eddb7dc5bcd 100644 --- a/plist +++ b/plist @@ -381,7 +381,9 @@ /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/forms/services.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Monit/forms/tests.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ExportController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/OpenVPN/ExportController.php +/usr/local/opnsense/mvc/app/controllers/OPNsense/OpenVPN/StatusController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/export_options.xml /usr/local/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/ServiceController.php /usr/local/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php @@ -710,6 +712,7 @@ /usr/local/opnsense/mvc/app/views/OPNsense/Monit/index.volt /usr/local/opnsense/mvc/app/views/OPNsense/Monit/status.volt /usr/local/opnsense/mvc/app/views/OPNsense/OpenVPN/export.volt +/usr/local/opnsense/mvc/app/views/OPNsense/OpenVPN/status.volt /usr/local/opnsense/mvc/app/views/OPNsense/Proxy/index.volt /usr/local/opnsense/mvc/app/views/OPNsense/Routes/index.volt /usr/local/opnsense/mvc/app/views/OPNsense/Syslog/index.volt @@ -922,6 +925,7 @@ /usr/local/opnsense/scripts/openssh/ssh_query.py /usr/local/opnsense/scripts/openvpn/client_connect.php /usr/local/opnsense/scripts/openvpn/client_disconnect.sh +/usr/local/opnsense/scripts/openvpn/kill_session.py /usr/local/opnsense/scripts/openvpn/ovpn_event.py /usr/local/opnsense/scripts/openvpn/ovpn_status.py /usr/local/opnsense/scripts/openvpn/tls_verify.php @@ -1956,7 +1960,6 @@ /usr/local/www/status_habackup.php /usr/local/www/status_interfaces.php /usr/local/www/status_ntpd.php -/usr/local/www/status_openvpn.php /usr/local/www/status_wireless.php /usr/local/www/system_advanced_admin.php /usr/local/www/system_advanced_firewall.php diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php new file mode 100644 index 00000000000..b723181ede3 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php @@ -0,0 +1,221 @@ +object(); + $config_payload = []; + $cnf_section = 'openvpn-' . $role; + if (!empty($config->openvpn->$cnf_section)) { + foreach ($config->openvpn->$cnf_section as $cnf) { + if (!empty((string)$cnf->vpnid)) { + $config_payload[(string)$cnf->vpnid] = $cnf; + } + } + } + return $config_payload; + } + + /** + * Search sessions + * @return array + */ + public function searchSessionsAction() + { + $this->sessionClose(); + $data = json_decode((new Backend())->configdRun('openvpn connections client,server') ?? '', true) ?? []; + $records = []; + $roles = ['client', 'server']; + if ($this->request->has('type') && is_array($this->request->get('type'))) { + $roles = array_intersect($this->request->get('type'), $roles); + } + foreach ($roles as $role) { + $config_payload = $this->getConfigs($role); + $vpnids = []; + if (!empty($data[$role])) { + foreach ($data[$role] as $idx => $stats) { + $vpnids[] = $idx; + $stats['type'] = $role; + $stats['id'] = $idx; + $stats['description'] = ''; + $stats['connected_since'] = null; + if (!empty($stats['timestamp'])) { + $stats['connected_since'] = date('Y-m-d H:i:s', $stats['timestamp']); + } + if (!empty($config_payload[$idx])) { + $stats['description'] = (string)$config_payload[$idx]->description ?? ''; + } + if (!empty($stats['client_list'])) { + foreach ($stats['client_list'] as $client) { + $tmp = array_merge($stats, $client); + $tmp['id'] .= '_' . $client['real_address']; + $tmp['is_client'] = true; + $records[] = $tmp; + } + } else { + $records[] = $stats; + } + } + } + // add non running enabled servers + foreach ($config_payload as $idx => $cnf) { + if (!in_array($idx, $vpnids) && empty((string)$cnf->disable)) { + $records[] = [ + 'id' => $idx, + 'service_id' => "openvpn/" . $idx, + 'type' => $role, + 'description' => (string)$cnf->description ?? '', + 'connected_since' => null, + 'status' => null + ]; + } + } + } + return $this->searchRecordsetBase($records); + } + + /** + * Search routes + * @return array + */ + public function searchRoutesAction() + { + $records = []; + $data = json_decode((new Backend())->configdRun('openvpn connections client,server') ?? '', true) ?? []; + $records = []; + $roles = ['client', 'server']; + if ($this->request->has('type') && is_array($this->request->get('type'))) { + $roles = array_intersect($this->request->get('type'), $roles); + } + foreach ($roles as $role) { + if (!empty($data[$role])) { + $config_payload = $this->getConfigs($role); + foreach ($data[$role] as $idx => $payload) { + if (!empty($payload['routing_table'])) { + foreach ($payload['routing_table'] as $route_entry) { + $route_entry['type'] = $role; + $route_entry['id'] = $idx; + $route_entry['description'] = ''; + if (!empty($config_payload[$idx])) { + $route_entry['description'] = (string)$config_payload[$idx]->description ?? ''; + } + $records[] = $route_entry; + } + } + } + } + } + return $this->searchRecordsetBase($records); + } + + /** + * kill session by source ip:port or common name + * @return array + */ + public function killSessionAction() + { + if (!$this->request->isPost()) { + return ['result' => 'failed']; + } + $this->sessionClose(); + $server_id = $this->request->get('server_id', null); + $session_id = $this->request->get('session_id', null); + if ($server_id != null && $session_id != null) { + $data = json_decode((new Backend())->configdpRun('openvpn kill', [$server_id, $session_id]) ?? '', true); + if (!empty($data)) { + return $data; + } + return ['result' => 'failed']; + } else { + return ['status' => 'invalid']; + } + } + + /** + * @param int $id server/client id to start + * @return array + */ + public function startServiceAction($id = null) + { + if (!$this->request->isPost() || $id == null) { + return ['result' => 'failed']; + } + + $this->sessionClose(); + + (new Backend())-> configdpRun('service start', ['openvpn', $id]); + + return ['result' => 'ok']; + } + + /** + * @param int $id server/client id to stop + * @return array + */ + public function stopServiceAction($id = null) + { + if (!$this->request->isPost() || $id == null) { + return ['result' => 'failed']; + } + + $this->sessionClose(); + + (new Backend())-> configdpRun('service stop', ['openvpn', $id]); + + return ['result' => 'ok']; + } + + /** + * @param int $id server/client id to restart + * @return array + */ + public function restartServiceAction($id = null) + { + if (!$this->request->isPost() || $id == null) { + return ['result' => 'failed']; + } + + $this->sessionClose(); + + (new Backend())-> configdpRun('service restart', ['openvpn', $id]); + + return ['result' => 'ok']; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/StatusController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/StatusController.php new file mode 100644 index 00000000000..020d7c54e99 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/StatusController.php @@ -0,0 +1,47 @@ +view->pick('OPNsense/OpenVPN/status'); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index c3f6b7bc6cc..a37c8b78e44 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -514,7 +514,8 @@ Status: OpenVPN - status_openvpn.php* + ui/openvpn/status + api/openvpn/service/* diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index 758ea4dcc9a..86b166d949b 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -217,7 +217,7 @@ - + diff --git a/src/opnsense/mvc/app/views/OPNsense/OpenVPN/status.volt b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/status.volt new file mode 100644 index 00000000000..d476fcd6855 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/status.volt @@ -0,0 +1,161 @@ +{# + # Copyright (c) 2023 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Type') }}{{ lang._('Description') }}{{ lang._('Common Name') }}{{ lang._('Real Address') }}{{ lang._('Virtual Address') }}{{ lang._('Connected Since') }}{{ lang._('Bytes Sent') }}{{ lang._('Bytes Received') }}{{ lang._('Status') }}
+
+
+ + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Type') }}{{ lang._('Description') }}{{ lang._('Common Name') }}{{ lang._('Real Address') }}{{ lang._('Target Network') }}{{ lang._('Last referenced') }}
+
+
diff --git a/src/opnsense/scripts/openvpn/kill_session.py b/src/opnsense/scripts/openvpn/kill_session.py new file mode 100755 index 00000000000..0bf0a103a45 --- /dev/null +++ b/src/opnsense/scripts/openvpn/kill_session.py @@ -0,0 +1,82 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2023 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +""" + +import argparse +import glob +import socket +import re +import os +import ujson +socket.setdefaulttimeout(5) + + + +def ovpn_cmd(filename, cmd): + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(filename) + except socket.error: + return None + + sock.send(('%s\n'%cmd).encode()) + buffer = '' + while True: + try: + buffer += sock.recv(65536).decode() + except socket.timeout: + break + eob = buffer[-200:] + if eob.find('END') > -1 or eob.find('ERROR') > -1 or eob.find('SUCCESS') > -1: + break + sock.close() + return buffer + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('server_id', help='server/client id (where to find socket)', type=int) + parser.add_argument('session_id', help='session id (address+port) or common name') + args = parser.parse_args() + socket_name = None + for filename in glob.glob("/var/etc/openvpn/*.sock"): + basename = os.path.basename(filename) + if basename in ['client%d.sock'%args.server_id, 'server%d.sock'%args.server_id]: + socket_name = filename + break + if socket_name: + res = ovpn_cmd(socket_name, 'kill %s\n' % args.session_id) + if res.find('SUCCESS:') >= 0: + clients = 0 + for tmp in res.strip().split('\n')[-1].split(): + if tmp.isdigit(): + clients = int(tmp) + print(ujson.encode({'status': 'killed', 'clients': clients})) + else: + print(ujson.encode({'status': 'not_found'})) + else: + print(ujson.encode({'status': 'server_not_found'})) diff --git a/src/opnsense/scripts/openvpn/ovpn_status.py b/src/opnsense/scripts/openvpn/ovpn_status.py index 37b96a5ced0..7d6613364d9 100755 --- a/src/opnsense/scripts/openvpn/ovpn_status.py +++ b/src/opnsense/scripts/openvpn/ovpn_status.py @@ -99,7 +99,7 @@ def ovpn_state(filename): if len(tmp) > 2 and tmp[0].isdigit(): response['timestamp'] = int(tmp[0]) response['status'] = tmp[1].lower() - response['virtual_addr'] = tmp[3] if len(tmp) > 3 else "" + response['virtual_address'] = tmp[3] if len(tmp) > 3 else "" response['remote_host'] = tmp[4] if len(tmp) > 4 else "" return response diff --git a/src/opnsense/service/conf/actions.d/actions_openvpn.conf b/src/opnsense/service/conf/actions.d/actions_openvpn.conf index 96dcb104bf1..134037b72ad 100644 --- a/src/opnsense/service/conf/actions.d/actions_openvpn.conf +++ b/src/opnsense/service/conf/actions.d/actions_openvpn.conf @@ -3,3 +3,9 @@ command:/usr/local/opnsense/scripts/openvpn/ovpn_status.py parameters: --option %s type:script_output message:Query OpenVPN status (%s) + +[kill] +command:/usr/local/opnsense/scripts/openvpn/kill_session.py +parameters: %s %s +type:script_output +message:Kill OpenVPN session %s - %s diff --git a/src/www/status_openvpn.php b/src/www/status_openvpn.php deleted file mode 100644 index 6f12af9348d..00000000000 --- a/src/www/status_openvpn.php +++ /dev/null @@ -1,304 +0,0 @@ - - * Copyright (C) 2010 Jim Pingle - * Copyright (C) 2008 Shrew Soft Inc. - * Copyright (C) 2005 Scott Ullrich - * Copyright (C) 2005 Colin Smith - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, - * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -require_once("guiconfig.inc"); -require_once("interfaces.inc"); -require_once("plugins.inc.d/openvpn.inc"); - -function kill_client($port, $client = null) -{ - $tcpsrv = "unix:///var/etc/openvpn/{$port}.sock"; - $errval = ''; - $errstr = ''; - - /* open a tcp connection to the management port of each server */ - $fp = @stream_socket_client($tcpsrv, $errval, $errstr, 1); - $killed = -1; - if ($fp) { - stream_set_timeout($fp, 1); - fputs($fp, "kill {$client}\n"); - while (!feof($fp)) { - $line = fgets($fp, 1024); - - $info = stream_get_meta_data($fp); - if ($info['timed_out']) { - break; - } - /* parse header list line */ - if (strpos($line, "INFO:") !== false) { - continue; - } - if (strpos($line, "SUCCESS") !== false) { - $killed = 0; - } - break; - } - fclose($fp); - } - return $killed; -} - -if ($_SERVER['REQUEST_METHOD'] === 'GET') { - $vpnid = 0; -} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (isset($_POST['action']) && $_POST['action'] == 'kill') { - $port = $_POST['port']; - $remipp = $_POST['remipp']; - $common_name = $_POST['common_name']; - if (!empty($port) && !empty($remipp)) { - $retval = kill_client($port, $remipp); - if ($retval == -1 && !empty($common_name)) { - // kill by common name when the address couldn't be killed - $retval = kill_client($port, $common_name); - echo html_safe("|{$port}|{$common_name}|{$retval}|"); - } else { - echo html_safe("|{$port}|{$remipp}|{$retval}|"); - } - } else { - echo gettext("invalid input"); - } - exit; - } -} - -$openvpn_status = json_decode(configd_run('openvpn connections client,server'), true) ?? []; -$openvpn_cfg = openvpn_config(); -legacy_html_escape_form_data($openvpn_status); -legacy_html_escape_form_data($openvpn_cfg); - -include("head.inc"); ?> - - - - - - -
-
-
-
- - $server): ?> -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "> - - - - - - - - - - - - -
- -
- $server['vpnid'])); ?> - - -
-
- -
- - - -
- - - - -
-
-
-
- - - -
-
- - - - - - - - - - - - - - - - - - "> - - - - - - - - - - - -
-
- $client['vpnid'])); ?> - - -
-
-
-
- - -
-
- - - - - - - - -
-
-
-
- - -
-
-
-
-

- '> @@ -89,7 +88,7 @@ elseif (!empty($server['timestamp'])):?> -
+
'>