diff --git a/src/etc/inc/filter.inc b/src/etc/inc/filter.inc index 19b250866..82111fb55 100644 --- a/src/etc/inc/filter.inc +++ b/src/etc/inc/filter.inc @@ -584,7 +584,9 @@ function filter_generate_scrubing(&$FilterIflist) if (!empty($config['filter']['scrub']['rule'])) { foreach ($config['filter']['scrub']['rule'] as $scrub_rule) { if (!isset($scrub_rule['disabled'])) { - $scrub_rule_out = "scrub on "; + $scrub_rule_out = "scrub"; + $scrub_rule_out .= !empty($scrub_rule['direction']) ? " " . $scrub_rule['direction'] : "" ; + $scrub_rule_out .= " on "; $interfaces = array(); foreach (explode(',', $scrub_rule['interface']) as $interface) { if (!empty($FilterIflist[$interface]['if'])) { @@ -603,6 +605,7 @@ function filter_generate_scrubing(&$FilterIflist) } else { $scrub_rule_out .= "any"; } + $scrub_rule_out .= !empty($scrub_rule['srcport']) ? " port " . $scrub_rule['srcport'] : ""; $scrub_rule_out .= " to "; if (is_alias($scrub_rule['dst'])) { $scrub_rule_out .= !empty($scrub_rule['dstnot']) ? "!" : ""; diff --git a/src/etc/inc/plugins.inc b/src/etc/inc/plugins.inc index 7ccce476e..87e926396 100644 --- a/src/etc/inc/plugins.inc +++ b/src/etc/inc/plugins.inc @@ -94,11 +94,21 @@ function plugins_syslog() * Every _interface should return a named array containing the interface unique identifier and properties. * */ -function plugins_interfaces() +function plugins_interfaces($write_allowed = true) { global $config; - $changed_interfaces = array(); - $registered_interfaces = array(); + + $stale_interfaces = array(); + $write_required = false; + + // mark previous dynamic registrations stale + if (isset($config['interfaces'])) { + foreach ($config['interfaces'] as $intf_ref => $intf_data) { + if (isset($intf_data[0]['internal_dynamic']) || isset($intf_data['internal_dynamic'])) { + $stale_interfaces[$intf_ref] = 1; + } + } + } // register / update interfaces foreach (plugins_scan() as $name => $path) { @@ -107,8 +117,9 @@ function plugins_interfaces() if (function_exists($func)) { foreach ($func() as $intf_ref => $intf_data) { if (is_array($intf_data)) { - if (!in_array($intf_ref, $registered_interfaces)) { - $registered_interfaces[] = $intf_ref; + // mark interface used + if (isset($stale_interfaces[$intf_ref])) { + unset($stale_interfaces[$intf_ref]); } if (empty($config['interfaces'][$intf_ref])) { $config['interfaces'][$intf_ref] = array(); @@ -124,9 +135,7 @@ function plugins_interfaces() foreach ($intf_data as $prop_name => $prop_value) { if ((empty($intf_config[$prop_name]) && !empty($prop_value)) || $intf_config[$prop_name] != $prop_value) { $intf_config[$prop_name] = $prop_value; - if (!in_array($intf_ref, $changed_interfaces)) { - $changed_interfaces[] = $intf_ref; - } + $write_required = true; } } } @@ -135,17 +144,15 @@ function plugins_interfaces() } // cleanup registrations - if (isset($config['interfaces'])) { - foreach ($config['interfaces'] as $intf => $intf_data) { - if (!empty($intf_data['internal_dynamic']) && !in_array($intf, $registered_interfaces)) { - $changed_interfaces[] = $intf; - unset($config['interfaces'][$intf]); - } + foreach ($stale_interfaces as $intf_ref => $no_data) { + if (isset($config['interfaces'][$intf_ref])) { + unset($config['interfaces'][$intf_ref]); + $write_required = true; } } // configuration changed, materialize - if (count($changed_interfaces) > 0) { + if ($write_allowed && $write_required) { write_config(); } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiModelControllerBase.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiModelControllerBase.php new file mode 100644 index 000000000..a349ecd9a --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiModelControllerBase.php @@ -0,0 +1,110 @@ +internalModelClass)) { + throw new \Exception('cannot instantiate without internalModelClass defined.'); + } + if (empty($this->internalModelName)) { + throw new \Exception('cannot instantiate without internalModelName defined.'); + } + } + + /** + * retrieve model settings + * @return array settings + */ + public function getAction() + { + // define list of configurable settings + $result = array(); + if ($this->request->isGet()) { + $mdl = $this->getModel(); + $result[$this->internalModelName] = $this->getModelNodes(); + } + return $result; + } + + /** + * override this to customize what part of the model gets exposed + * @return array + */ + protected function getModelNodes() + { + return $this->getModel()->getNodes(); + } + + /** + * override this to customize the model binding behavior + * @return null|BaseModel + */ + protected function getModel() + { + if ($this->modelHandle == null) { + $this->modelHandle = (new \ReflectionClass($this->internalModelClass))->newInstance(); + } + + return $this->modelHandle; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiMutableModelControllerBase.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiMutableModelControllerBase.php new file mode 100644 index 000000000..412116904 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiMutableModelControllerBase.php @@ -0,0 +1,74 @@ +"failed"); + if ($this->request->isPost()) { + // load model and update with provided data + $mdl = $this->getModel(); + $mdl->setNodes($this->request->getPost($this->internalModelName)); + + // perform validation + $valMsgs = $mdl->performValidation(); + foreach ($valMsgs as $field => $msg) { + if (!array_key_exists("validations", $result)) { + $result["validations"] = array(); + } + $result["validations"][$this->internalModelName.".".$msg->getField()] = $msg->getMessage(); + } + + // serialize model to config and save + if ($valMsgs->count() == 0) { + $mdl->serializeToConfig(); + Config::getInstance()->save(); + $result["result"] = "saved"; + } + } + return $result; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IDS/Api/SettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IDS/Api/SettingsController.php index e4c111254..c564caf2c 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/IDS/Api/SettingsController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/IDS/Api/SettingsController.php @@ -147,15 +147,19 @@ class SettingsController extends ApiControllerBase /** * get rule information - * @param $sid rule identifier + * @param string|null $sid rule identifier * @return array|mixed */ - public function getRuleInfoAction($sid) + public function getRuleInfoAction($sid=null) { // request list of installed rules - $backend = new Backend(); - $response = $backend->configdpRun("ids query rules", array(1, 0,'sid/'.$sid)); - $data = json_decode($response, true); + if (!empty($sid)) { + $backend = new Backend(); + $response = $backend->configdpRun("ids query rules", array(1, 0,'sid/'.$sid)); + $data = json_decode($response, true); + } else { + $data = null; + } if ($data != null && array_key_exists("rows", $data) && count($data['rows'])>0) { $row = $data['rows'][0]; diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php index 41c3c1d4a..94d14328a 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Proxy/Api/SettingsController.php @@ -28,8 +28,7 @@ */ namespace OPNsense\Proxy\Api; -use \OPNsense\Base\ApiControllerBase; -use \OPNsense\Proxy\Proxy; +use \OPNsense\Base\ApiMutableModelControllerBase; use \OPNsense\Cron\Cron; use \OPNsense\Core\Config; use \OPNsense\Base\UIModelGrid; @@ -38,57 +37,10 @@ use \OPNsense\Base\UIModelGrid; * Class SettingsController * @package OPNsense\Proxy */ -class SettingsController extends ApiControllerBase +class SettingsController extends ApiMutableModelControllerBase { - /** - * retrieve proxy settings - * @return array - */ - public function getAction() - { - $result = array(); - if ($this->request->isGet()) { - $mdlProxy = new Proxy(); - $result['proxy'] = $mdlProxy->getNodes(); - } - - return $result; - } - - - /** - * update proxy configuration fields - * @return array - * @throws \Phalcon\Validation\Exception - */ - public function setAction() - { - $result = array("result"=>"failed"); - if ($this->request->hasPost("proxy")) { - // load model and update with provided data - $mdlProxy = new Proxy(); - $mdlProxy->setNodes($this->request->getPost("proxy")); - - // perform validation - $valMsgs = $mdlProxy->performValidation(); - foreach ($valMsgs as $field => $msg) { - if (!array_key_exists("validations", $result)) { - $result["validations"] = array(); - } - $result["validations"]["proxy.".$msg->getField()] = $msg->getMessage(); - } - - // serialize model to config and save - if ($valMsgs->count() == 0) { - $mdlProxy->serializeToConfig(); - $cnf = Config::getInstance(); - $cnf->save(); - $result["result"] = "saved"; - } - } - - return $result; - } + protected $internalModelName = 'proxy'; + protected $internalModelClass = '\OPNsense\Proxy\Proxy'; /** * @@ -98,7 +50,7 @@ class SettingsController extends ApiControllerBase public function searchRemoteBlacklistsAction() { $this->sessionClose(); - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); $grid = new UIModelGrid($mdlProxy->forward->acl->remoteACLs->blacklists->blacklist); return $grid->fetchBindRequest( $this->request, @@ -114,7 +66,7 @@ class SettingsController extends ApiControllerBase */ public function getRemoteBlacklistAction($uuid = null) { - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); if ($uuid != null) { $node = $mdlProxy->getNodeByReference('forward.acl.remoteACLs.blacklists.blacklist.' . $uuid); if ($node != null) { @@ -139,7 +91,7 @@ class SettingsController extends ApiControllerBase public function setRemoteBlacklistAction($uuid) { if ($this->request->isPost() && $this->request->hasPost("blacklist")) { - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); if ($uuid != null) { $node = $mdlProxy->getNodeByReference('forward.acl.remoteACLs.blacklists.blacklist.' . $uuid); if ($node != null) { @@ -175,7 +127,7 @@ class SettingsController extends ApiControllerBase $result = array("result" => "failed"); if ($this->request->isPost() && $this->request->hasPost("blacklist")) { $result = array("result" => "failed", "validations" => array()); - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); $node = $mdlProxy->forward->acl->remoteACLs->blacklists->blacklist->Add(); $node->setNodes($this->request->getPost("blacklist")); $valMsgs = $mdlProxy->performValidation(); @@ -207,7 +159,7 @@ class SettingsController extends ApiControllerBase $result = array("result" => "failed"); if ($this->request->isPost()) { - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); if ($uuid != null) { if ($mdlProxy->forward->acl->remoteACLs->blacklists->blacklist->del($uuid)) { // if item is removed, serialize to config and save @@ -233,7 +185,7 @@ class SettingsController extends ApiControllerBase $result = array("result" => "failed"); if ($this->request->isPost()) { - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); if ($uuid != null) { $node = $mdlProxy->getNodeByReference('forward.acl.remoteACLs.blacklists.blacklist.' . $uuid); if ($node != null) { @@ -262,7 +214,7 @@ class SettingsController extends ApiControllerBase $result = array("result" => "failed"); if ($this->request->isPost()) { - $mdlProxy = new Proxy(); + $mdlProxy = $this->getModel(); if ((string)$mdlProxy->forward->acl->remoteACLs->UpdateCron == "") { $mdlCron = new Cron(); // update cron relation (if this doesn't break consistency) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml b/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml index 3b4e407ce..0b02a4883 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Proxy/forms/main.xml @@ -166,25 +166,25 @@ proxy.general.traffic.maxDownloadSize - + text proxy.general.traffic.maxUploadSize - + text proxy.general.traffic.OverallBandwidthTrotteling - + text proxy.general.traffic.perHostTrotteling - + text diff --git a/src/opnsense/mvc/app/models/OPNsense/IDS/IDS.php b/src/opnsense/mvc/app/models/OPNsense/IDS/IDS.php index efef19fd3..dcee0ca51 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IDS/IDS.php +++ b/src/opnsense/mvc/app/models/OPNsense/IDS/IDS.php @@ -131,7 +131,7 @@ class IDS extends BaseModel public function getRuleStatus($sid, $default) { $this->updateSIDlist(); - if (array_key_exists($sid, $this->sid_list)) { + if (!empty($sid) && array_key_exists($sid, $this->sid_list)) { return (string)$this->sid_list[$sid]->enabled; } else { return $default; @@ -148,7 +148,7 @@ class IDS extends BaseModel public function getRuleAction($sid, $default, $response_plain = false) { $this->updateSIDlist(); - if (array_key_exists($sid, $this->sid_list)) { + if (!empty($sid) && array_key_exists($sid, $this->sid_list)) { if (!$response_plain) { return $this->sid_list[$sid]->action->getNodeData(); } else { diff --git a/src/opnsense/scripts/netflow/flowd_aggregate.py b/src/opnsense/scripts/netflow/flowd_aggregate.py index 8915576f7..1368046ed 100755 --- a/src/opnsense/scripts/netflow/flowd_aggregate.py +++ b/src/opnsense/scripts/netflow/flowd_aggregate.py @@ -28,7 +28,6 @@ Aggregate flowd data for reporting """ import time -import datetime import os import sys import signal @@ -37,6 +36,7 @@ import copy import syslog import traceback sys.path.insert(0, "/usr/local/opnsense/site-python") +from sqlite3_helper import check_and_repair from lib.parse import parse_flow from lib.aggregate import AggMetadata import lib.aggregates @@ -130,6 +130,9 @@ class Main(object): """ run, endless loop, until sigterm is received :return: None """ + # check database consistency / repair + check_and_repair('/var/netflow/*.sqlite') + vacuum_interval = (60*60*8) # 8 hour vacuum cycle vacuum_countdown = None while self.running: diff --git a/src/opnsense/site-python/sqlite3_helper.py b/src/opnsense/site-python/sqlite3_helper.py new file mode 100644 index 000000000..9e0ee9ef5 --- /dev/null +++ b/src/opnsense/site-python/sqlite3_helper.py @@ -0,0 +1,69 @@ +""" + Copyright (c) 2016 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. + + -------------------------------------------------------------------------------------- + SQLite3 support functions +""" +import datetime +import glob +import sqlite3 +import syslog +import os + +def check_and_repair(filename_mask): + """ check and repair sqlite databases + :param filename_mask: filenames (glob pattern) + :return: None + """ + for filename in glob.glob(filename_mask): + try: + conn = sqlite3.connect(filename, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) + cur = conn.cursor() + cur.execute("SELECT name FROM sqlite_master where type = 'table'") + except sqlite3.DatabaseError: + # unrecoverable, doesn't look like a database, rename to .bck + filename_tmp = '%s.%s.bck'%(filename, datetime.datetime.now().strftime("%Y%m%d%H%M%S")) + syslog.syslog(syslog.LOG_ERR, "sqlite3 %s doesn't look like a database, renamed to %s " % (filename, + filename_tmp)) + cur = None + os.rename(filename, filename_tmp) + + # try to vacuum all tables, triggers a "database disk image is malformed" when corrupted + # force a repair when corrupted, using a dump / import + if cur is not None: + try: + for table in cur.fetchall(): + cur.execute('vacuum %s' % table[0]) + except sqlite3.DatabaseError, e: + if e.message.find('malformed') > -1: + syslog.syslog(syslog.LOG_ERR, "sqlite3 repair %s" % filename) + filename_tmp = '%s.fix'%filename + if os.path.exists(filename_tmp): + os.remove(filename_tmp) + os.system('echo ".dump" | /usr/local/bin/sqlite3 %s | /usr/local/bin/sqlite3 %s' % (filename, + filename_tmp)) + if os.path.exists(filename_tmp): + os.remove(filename) + os.rename(filename_tmp, filename) diff --git a/src/www/diag_packet_capture.php b/src/www/diag_packet_capture.php index beb39e33a..4f175658e 100644 --- a/src/www/diag_packet_capture.php +++ b/src/www/diag_packet_capture.php @@ -154,7 +154,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { header("Content-Type: application/octet-stream"); header("Content-Disposition: attachment; filename=packetcapture.cap"); header("Content-Length: ".filesize("/root/packetcapture.cap")); - readfile("/root/packetcapture.cap"); + $file = fopen("/root/packetcapture.cap", "r"); + while(!feof($file)) { + print(fread($file, 32 * 1024)); + ob_flush(); + } + fclose($file); exit; } elseif (!empty($_GET['view'])) { // download capture contents diff --git a/src/www/firewall_scrub_edit.php b/src/www/firewall_scrub_edit.php index d055691ce..0392e9f51 100644 --- a/src/www/firewall_scrub_edit.php +++ b/src/www/firewall_scrub_edit.php @@ -53,7 +53,8 @@ $a_scrub = &$config['filter']['scrub']['rule']; // define form fields $config_fields = array('interface', 'proto', 'srcnot', 'src', 'srcmask', 'dstnot', 'dst', 'dstmask', 'dstport', - 'no-df', 'random-id', 'max-mss', 'min-ttl', 'set-tos', 'descr', 'disabled'); + 'no-df', 'random-id', 'max-mss', 'min-ttl', 'set-tos', 'descr', 'disabled', 'direction', + 'srcport'); if ($_SERVER['REQUEST_METHOD'] === 'GET') { // input record id, if valid @@ -102,6 +103,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (!empty($pconfig['dstport']) && $pconfig['dstport'] != 'any' && !is_portoralias($pconfig['dstport']) && !is_portrange($pconfig['dstport'])) { $input_errors[] = sprintf(gettext("%s doesn't appear to be a valid port number, alias or range"), $pconfig['dstport']) ; } + if (!empty($pconfig['srcport']) && $pconfig['srcport'] != 'any' && !is_portoralias($pconfig['srcport']) && !is_portrange($pconfig['srcport'])) { + $input_errors[] = sprintf(gettext("%s doesn't appear to be a valid port number, alias or range"), $pconfig['srcport']) ; + } if (is_ipaddrv4($pconfig['src']) && is_ipaddrv6($pconfig['dst'])) { $input_errors[] = gettext("You can not use IPv6 addresses in IPv4 rules."); } @@ -220,15 +224,28 @@ include("head.inc"); // lock src/dst ports on other then tcp/udp if ($("#proto").val() == 'tcp' || $("#proto").val() == 'udp' || $("#proto").val() == 'tcp/udp') { $("#dstport").prop('disabled', false); + $("#srcport").prop('disabled', false); } else { $("#dstport optgroup:last option:first").prop('selected', true); $("#dstport").prop('disabled', true); + $("#srcport").prop('disabled', true); } $("#dstport").selectpicker('refresh'); $("#dstport").change(); + $("#srcport").selectpicker('refresh'); + $("#srcport").change(); }); $("#proto").change(); + if ($("#srcport").val() != "") { + $("#show_srcport").show(); + $("#show_srcport_adv").parent().hide(); + } + $("#show_srcport_adv").click(function(){ + $("#show_srcport").show(); + $("#show_srcport_adv").parent().hide(); + }); + // IPv4/IPv6 select hook_ipv4v6('ipv4v6net', 'network-id'); }); @@ -286,24 +303,22 @@ include("head.inc"); - - @@ -379,6 +394,48 @@ include("head.inc"); + + + +
+ " id="show_srcport_adv" /> +
+ + + + diff --git a/src/www/interfaces_groups.php b/src/www/interfaces_groups.php index ff08fc21e..5ffebd400 100644 --- a/src/www/interfaces_groups.php +++ b/src/www/interfaces_groups.php @@ -53,8 +53,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } unset($a_ifgroups[$id]); + plugins_interfaces(false); write_config(); - plugins_interfaces(); header("Location: interfaces_groups.php"); exit; } diff --git a/src/www/interfaces_groups_edit.php b/src/www/interfaces_groups_edit.php index c9ba3f079..6e1fab074 100644 --- a/src/www/interfaces_groups_edit.php +++ b/src/www/interfaces_groups_edit.php @@ -125,9 +125,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { // add new item $a_ifgroups[] = $ifgroupentry; } + plugins_interfaces(false); write_config(); interface_group_setup($ifgroupentry); - plugins_interfaces(); header("Location: interfaces_groups.php"); exit; } diff --git a/src/www/load_balancer_pool_edit.php b/src/www/load_balancer_pool_edit.php index 1cfd73636..5441979c2 100644 --- a/src/www/load_balancer_pool_edit.php +++ b/src/www/load_balancer_pool_edit.php @@ -352,10 +352,12 @@ include("head.inc");
diff --git a/src/www/system_certmanager.php b/src/www/system_certmanager.php index 6ee49e27e..70844fa1f 100644 --- a/src/www/system_certmanager.php +++ b/src/www/system_certmanager.php @@ -464,6 +464,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { 'organizationName' => $pconfig['csr_dn_organization'], 'emailAddress' => $pconfig['csr_dn_email'], 'commonName' => $pconfig['csr_dn_commonname']); + if (!empty($pconfig['csr_dn_organizationalunit'])) { + $dn['organizationalUnitName'] = $pconfig['csr_dn_organizationalunit']; + } if (count($altnames)) { $altnames_tmp = ""; foreach ($altnames as $altname) { @@ -1056,6 +1059,17 @@ $( document ).ready(function() { + + :   + + + + + :   diff --git a/src/www/vpn_openvpn_export.php b/src/www/vpn_openvpn_export.php index f2bf85ca2..8a7ca8fdc 100644 --- a/src/www/vpn_openvpn_export.php +++ b/src/www/vpn_openvpn_export.php @@ -559,9 +559,6 @@ if (isset($savemsg)) { - -   - @@ -701,18 +698,21 @@ if (isset($savemsg)) { + + + + -
+ -
+ -
+ -
+ : or -
+ - + +