VPN: OpenVPN: Connection Status - refactor to MVC closes https://github.com/opnsense/core/issues/6382

o rename virtual_addr --> virtual_address in status call out
o add new endpoints to search connections and routes, kill sessions and service control
o remove old status page status_openvpn.php and change ACL and Menu registration
o offer two tab view on sessions / routes
o service controls (restart/start/stop) are shown for non client based records (p2p and client mode) or when no clients are connected.
This commit is contained in:
Ad Schellevis 2023-03-19 15:52:09 +01:00
parent 2d31af2a5e
commit b9a1633a18
11 changed files with 528 additions and 309 deletions

5
plist
View File

@ -384,7 +384,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
@ -720,6 +722,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
@ -935,6 +938,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
@ -1969,7 +1973,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

View File

@ -0,0 +1,221 @@
<?php
/*
* 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.
*/
namespace OPNsense\OpenVPN\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Config;
use OPNsense\Core\Backend;
/**
* Class ServiceController
* @package OPNsense\OpenVPN
*/
class ServiceController extends ApiControllerBase
{
private function getConfigs($role)
{
$config = Config::getInstance()->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()) {
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()) {
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()) {
return ['result' => 'failed'];
}
$this->sessionClose();
(new Backend())-> configdpRun('service restart', ['openvpn', $id]);
return ['result' => 'ok'];
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* 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.
*/
namespace OPNsense\OpenVPN;
use OPNsense\Base\IndexController as BaseIndexController;
/**
* Class StatusController
* @package OPNsense\OpenVPN
*/
class StatusController extends BaseIndexController
{
/**
* default index page
* @throws \Exception
*/
public function indexAction()
{
$this->view->pick('OPNsense/OpenVPN/status');
}
}

View File

@ -515,7 +515,8 @@
<page-status-openvpn>
<name>Status: OpenVPN</name>
<patterns>
<pattern>status_openvpn.php*</pattern>
<pattern>ui/openvpn/status</pattern>
<pattern>api/openvpn/service/*</pattern>
</patterns>
</page-status-openvpn>
<page-status-services>

View File

@ -217,7 +217,7 @@
<ClientExport order="40" VisibleName="Client Export" url="/ui/openvpn/export">
<Edit url="/ui/openvpn/export?*" visibility="hidden"/>
</ClientExport>
<Status order="60" VisibleName="Connection Status" url="/status_openvpn.php"/>
<Status order="60" VisibleName="Connection Status" url="/ui/openvpn/status"/>
<LogFile order="70" VisibleName="Log File" url="/ui/diagnostics/log/core/openvpn"/>
</OpenVPN>
</VPN>

View File

@ -0,0 +1,162 @@
{#
OPNsense® is Copyright © 2023 by 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.
#}
<script>
'use strict';
$( document ).ready(function () {
let grid_sessions = $("#grid-sessions").UIBootgrid({
search:'/api/openvpn/service/search_sessions',
options:{
selection: false,
formatters:{
commands: function (column, row) {
if (row.is_client) {
return '<button type="button" class="btn btn-xs btn-default ovpn-command command-kill" data-toggle="tooltip" title="{{ lang._('Kill') }}" data-common_name="'+row.common_name+'" data-row-id="' + row.id + '"><span class="fa fa-times fa-fw"></span></button>';
} else if (row.status === null) {
return '<button type="button" class="btn btn-xs btn-default ovpn-command command-start" data-toggle="tooltip" title="{{ lang._('Start') }}" data-row-id="' + row.id + '"><span class="fa fa-play fa-fw"></span></button>';
} else {
return '<button type="button" class="btn btn-xs btn-default ovpn-command command-restart" data-toggle="tooltip" title="{{ lang._('Restart') }}" data-row-id="' + row.id + '"><span class="fa fa-repeat fa-fw"></span></button>' +
'<button type="button" class="btn btn-xs btn-default ovpn-command command-stop" data-toggle="tooltip" title="{{ lang._('Stop') }}" data-row-id="' + row.id + '"><span class="fa fa-stop fa-fw"></span></button>';
}
}
},
requestHandler: function(request){
if ( $('#type_filter').val().length > 0) {
request['type'] = $('#type_filter').val();
}
return request;
},
}
});
$("#grid-routes").UIBootgrid({
search:'/api/openvpn/service/search_routes',
options:{
selection: false
}
});
grid_sessions.on('loaded.rs.jquery.bootgrid', function () {
$('[data-toggle="tooltip"]').tooltip();
$(".ovpn-command").click(function(){
let this_cmd = $(this);
if (this_cmd.hasClass('command-kill')) {
let tmp = this_cmd.data('row-id').split('_');
if (tmp.length == 2) {
let params = {server_id: tmp[0], session_id: tmp[1]};
ajaxCall('/api/openvpn/service/kill_session/', params, function(data, status){
if (data && data.status === 'not_found') {
// kill by common name
params.session_id = this_cmd.data('common_name');
ajaxCall('/api/openvpn/service/kill_session/', params, function(data, status){
$('#grid-sessions').bootgrid('reload');
});
} else{
$('#grid-sessions').bootgrid('reload');
}
});
}
} else if (this_cmd.hasClass('command-start')) {
ajaxCall('/api/openvpn/service/start_service/' + this_cmd.data('row-id'), {}, function(data, status){
$('#grid-sessions').bootgrid('reload');
});
} else if (this_cmd.hasClass('command-stop')) {
ajaxCall('/api/openvpn/service/stop_service/' + this_cmd.data('row-id'), {}, function(data, status){
$('#grid-sessions').bootgrid('reload');
});
} else if (this_cmd.hasClass('command-restart')) {
ajaxCall('/api/openvpn/service/restart_service/' + this_cmd.data('row-id'), {}, function(data, status){
$('#grid-sessions').bootgrid('reload');
});
}
});
});
$("#type_filter").change(function(){
$('#grid-sessions').bootgrid('reload');
});
$("#type_filter_container").detach().prependTo('#grid-sessions-header > .row > .actionBar > .actions');
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#sessions">{{ lang._('Sessions') }}</a></li>
<li><a data-toggle="tab" href="#routes">{{ lang._('Routes') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="sessions" class="tab-pane fade in active">
<div class="hidden">
<!-- filter per type container -->
<div id="type_filter_container" class="btn-group">
<select id="type_filter" data-title="{{ lang._('Filter type') }}" class="selectpicker" data-live-search="true" multiple="multiple" data-width="200px">
<option value="server">{{ lang._('Server') }}</option>
<option value="client">{{ lang._('Client') }}</option>
</select>
</div>
</div>
<table id="grid-sessions" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="id" data-type="string" data-sortable="false" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="type" data-type="string">{{ lang._('Type') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="common_name" data-type="string">{{ lang._('Common Name') }}</th>
<th data-column-id="real_address" data-type="string">{{ lang._('Real Address') }}</th>
<th data-column-id="virtual_address" data-type="string">{{ lang._('Virtual Address') }}</th>
<th data-column-id="connected_since" data-type="string">{{ lang._('Connected Since') }}</th>
<th data-column-id="bytes_sent" data-type="string">{{ lang._('Bytes Sent') }}</th>
<th data-column-id="bytes_received" data-type="string">{{ lang._('Bytes Received') }}</th>
<th data-column-id="status" data-type="string">{{ lang._('Status') }}</th>
<th data-column-id="commands" data-width="5em" data-formatter="commands" data-sortable="false"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id="routes" class="tab-pane fade in">
<table id="grid-routes" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="id" data-type="string" data-sortable="false" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="type" data-type="string">{{ lang._('Type') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="common_name" data-type="string">{{ lang._('Common Name') }}</th>
<th data-column-id="real_address" data-type="string">{{ lang._('Real Address') }}</th>
<th data-column-id="virtual_address" data-type="string">{{ lang._('Target Network') }}</th>
<th data-column-id="last_ref" data-type="string">{{ lang._('Last referenced') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,82 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2023 Ad Schellevis <ad@opnsense.org>
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'}))

View File

@ -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

View File

@ -3,3 +3,10 @@ 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

View File

@ -1,304 +0,0 @@
<?php
/*
* Copyright (C) 2014-2023 Deciso B.V.
* Copyright (C) 2019 Franco Fichtner <franco@opnsense.org>
* Copyright (C) 2010 Jim Pingle <jimp@pfsense.org>
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
* Copyright (C) 2005 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2005 Colin Smith <ethethlay@gmail.com>
* 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"); ?>
<body>
<?php include("fbegin.inc"); ?>
<script>
//<![CDATA[
$(document).ready(function () {
// link kill buttons
$(".act_kill_client").click(function (event) {
event.preventDefault();
var port = $(this).data("client-port");
var ip = $(this).data("client-ip");
let common_name = $(this).data("common_name");
$.post(window.location, {action: 'kill', port: port, remipp: ip, common_name:common_name}, function (data) {
location.reload();
});
});
// link show/hide routes
$(".act_show_routes").click(function () {
$("*[data-for='" + $(this).attr('id') + "']").toggle();
});
// minimize all buttons, some of the buttons come from the shared service
// functions, which outputs large buttons.
$(".btn").each(function () {
$(this).addClass("btn-xs");
});
});
//]]>
</script>
<section class="page-content-main">
<div class="container-fluid">
<div class="row">
<section class="col-xs-12">
<!-- XXX unused? <form method="get" name="iform">-->
<?php foreach ($openvpn_cfg['openvpn-server'] as $i => $server): ?>
<div class="tab-content content-box __mb">
<div class="table-responsive">
<table class="table table-striped">
<tbody>
<tr>
<td colspan="7">
<strong><?= $server['name'] ?> <?= gettext('Client connections') ?></strong>
<div class="pull-right">
<?php $ssvc = service_by_name('openvpn', array('id' => $server['vpnid'])); ?>
<?= service_control_icon($ssvc, true); ?>
<?= service_control_links($ssvc, true); ?>
</div>
</td>
</tr>
<tr>
<td><strong><?= gettext('Common Name') ?></strong></td>
<td><strong><?= gettext('Real Address') ?></strong></td>
<td><strong><?= gettext('Virtual Address') ?></strong></td>
<td><strong><?= gettext('Connected Since') ?></strong></td>
<td><strong><?= gettext('Bytes Sent') ?></strong></td>
<td><strong><?= gettext('Bytes Received') ?></strong></td>
<td><strong><?= gettext('Status') ?></strong></td>
</tr>
<?php if (empty($openvpn_status['server'][$server['vpnid']]) || empty($openvpn_status['server'][$server['vpnid']]['client_list'])): ?>
<?php if (!empty($openvpn_status['server'][$server['vpnid']]) && isset($openvpn_status['server'][$server['vpnid']]['write_bytes'])):?>
<?php $conn = $openvpn_status['server'][$server['vpnid']];?>
<tr>
<td><?= $conn['common_name'] ?? '' ?></td>
<td><?= $conn['real_address'] ?? '' ?></td>
<td><?= $conn['virtual_addr'] ?? '' ?></td>
<td><?= date('Y-m-d H:i:s', $conn['timestamp']) ?></td>
<td><?= format_bytes($conn['bytes_sent']) ?></td>
<td><?= format_bytes($conn['bytes_received']) ?></td>
<td><?= $conn['status'];?> </td>
</tr>
<?php else:?>
<tr>
<td colspan="7">
<?= gettext('No OpenVPN clients are connected to this instance.') ?>
</td>
</tr>
<?php endif ?>
<?php else: ?>
<?php foreach ($openvpn_status['server'][$server['vpnid']]['client_list'] as $conn): ?>
<tr>
<td><?= $conn['common_name'] ?></td>
<td><?= $conn['real_address'] ?></td>
<td><?= $conn['virtual_address'] ?></td>
<td><?= $conn['connected_since'] ?></td>
<td><?= format_bytes($conn['bytes_sent']) ?></td>
<td><?= format_bytes($conn['bytes_received']) ?></td>
<td>
<?php if ($conn['common_name'] != '[error]'): ?>
<button data-client-port="server<?= $server['vpnid']; ?>"
data-client-ip="<?= $conn['real_address']; ?>"
data-common_name="<?= $conn['common_name']; ?>"
title="<?= gettext("Kill client connection from") . " " . $conn['real_address']; ?>"
class="act_kill_client btn btn-default">
<i class="fa fa-times fa-fw"></i>
</button>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
<?php endif ?>
<?php if (!empty($openvpn_status['server'][$server['vpnid']]) && !empty($openvpn_status['server'][$server['vpnid']]['routing_table'])): ?>
<tr>
<td colspan="7">
<span style="cursor:pointer;" class="act_show_routes" id="showroutes_<?= $i ?>">
<i class="fa fa-chevron-down fa-fw"></i>
<strong><?= $server['name'] ?> <?= gettext('Routing Table') ?></strong>
</span>
</td>
</tr>
<tr style="display:none;" data-for="showroutes_<?= $i ?>">
<td><strong><?= gettext('Common Name') ?></strong></td>
<td><strong><?= gettext('Real Address') ?></strong></td>
<td><strong><?= gettext('Target Network') ?></strong></td>
<td><strong><?= gettext('Last Used') ?></strong></td>
<td colspan="3">
</tr>
<?php foreach ($openvpn_status['server'][$server['vpnid']]['routing_table'] as $conn): ?>
<tr style="display:none;" data-for="showroutes_<?= $i ?>" id="<?= html_safe("r:{$server['mgmt']}:{$conn['remote_host']}") ?>">
<td><?= $conn['common_name'] ?></td>
<td><?= $conn['real_address'] ?></td>
<td><?= $conn['virtual_address'] ?></td>
<td><?= $conn['last_ref'] ?></td>
<td colspan="3">
</tr>
<?php endforeach ?>
<tr style="display:none;" data-for="showroutes_<?= $i ?>">
<td colspan="7"><?= gettext("An IP address followed by C indicates a host currently connected through the VPN.") ?></td>
</tr>
</tr>
<?php endif ?>
</tbody>
</table>
</div>
</div>
<?php endforeach ?>
<?php if (!empty($openvpn_cfg['openvpn-client'])): ?>
<div class="tab-content content-box __mb">
<div class="table-responsive">
<table class="table table-striped">
<tbody>
<tr>
<td colspan="8"><strong><?= gettext('Client Instance Statistics') ?><strong></td>
</tr>
<tr>
<td><strong><?= gettext('Name') ?></strong></td>
<td><strong><?= gettext('Remote Host') ?></strong></td>
<td><strong><?= gettext('Virtual Addr') ?></strong></td>
<td><strong><?= gettext('Connected Since') ?></strong></td>
<td><strong><?= gettext('Bytes Sent') ?></strong></td>
<td><strong><?= gettext('Bytes Received') ?></strong></td>
<td><strong><?= gettext('Status') ?></strong></td>
<td></td>
</tr>
<?php foreach ($openvpn_cfg['openvpn-client'] as $client): ?>
<?php $conn = $openvpn_status['client'][$client['vpnid']] ?? [];?>
<tr id="<?= html_safe("r:{$client['port']}:{$client['vpnid']}") ?>">
<td><?= $client['name'] ?></td>
<td><?= $conn['remote_host'] ?? ''?></td>
<td><?= $conn['virtual_addr'] ?? '' ?></td>
<td><?= !empty($conn) ? date('Y-m-d H:i:s', $conn['timestamp']) : '' ?></td>
<td><?= format_bytes($conn['write_bytes'] ?? '0') ?></td>
<td><?= format_bytes($conn['read_bytes'] ?? '0') ?></td>
<td><?= !empty($conn) ? $conn['status'] : '' ?></td>
<td>
<div>
<?php $ssvc = service_by_name('openvpn', array('id' => $client['vpnid'])); ?>
<?= service_control_icon($ssvc, true); ?>
<?= service_control_links($ssvc, true); ?>
</div>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>
</div>
<?php endif ?>
<?php if (empty($openvpn_cfg['openvpn-server']) && empty($openvpn_cfg['openvpn-client'])): ?>
<div class="tab-content content-box __mb">
<div class="table-responsive">
<table class="table-responsive table table-striped">
<tbody>
<tr>
<td colspan="8"><strong><?= gettext('OpenVPN Status') ?></strong></th>
</tr>
<tr>
<td colspan="8"><?= gettext('No OpenVPN instance defined') ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<?php endif ?>
<!--</form>-->
</section>
</div>
</div>
</section>
<?php
include 'foot.inc';

View File

@ -89,7 +89,7 @@ foreach ($openvpn_cfg as $section => &$ovpncfg) {
elseif (!empty($server['timestamp'])):?>
<tr>
<td><?=date('Y-m-d H:i:s', $server['timestamp']);?></td>
<td><?=$server['remote_host'];?><br/><?=$server['virtual_addr'];?></td>
<td><?=$server['remote_host'];?><br/><?=$server['virtual_address'];?></td>
<td>
<span class='fa fa-exchange fa-fw <?=$server['status'] == "CONNECTED" ? "text-success" : "text-danger" ;?>'></span>
</td>