VPN: OpenVPN: Instances (MVC) (#6584)

* VPN: OpenVPN: Instances - add new module using the same approach as introduced for IPsec in 23.1. Since we likely can't easily migrate the old cruft, we better focus on offering the correct options for openvpn following upstream documentation.

o add boilerplate
o implement a solution to keep vpnid's unique so device creation for legacy and mvc can function in similar ways.
o add some of the main "helper" options for clients and servers
o Implement certificate logic, selecting a certificate also implies an authority (which we validate)
o hook CRL generation into the exising openvpn_refresh_crls() event
o attach already refactored authentication to new MVC as well, OpenVPN->getInstanceById() is responsible for feeding the data needed during authentication and overwrite generation.
o when in client mode and in need for a username+password combination, flush these to file and link in "auth-user-pass"
o routes (remote) and push routes (local), combine IPv4 and IPv6 for ease of administration,
o keep alive [push] ping-[restart] defined as seperate fields for validation
o add various "push" to client options in Miscellaneous section
o add "auth-gen-token" lifetime for https://github.com/opnsense/core/issues/6135
o allow selection of redirect-gateway type for https://github.com/opnsense/core/issues/6220
o move tls-auth/crypt into separate static keys objects (tab in instances page)
o hook existing events (ovpn_event.py) and make sure they locate the server using getServerById() when needed
o use getInstanceById in openvpn_prepare() to return both legacy as MVC device configuration
o add ovpn_service_control.php for service control [stop|start|restart|configure] and glue this in openvpn_services() via configd
o change openvpn_interfaces() to use isEnabled() method on the model to query if any (legacy/mvc) instances are enabled
o move openvpn_config() from openvpn.inc to widget and extend with MVC instances
o extend ovpn_status.py to parse "instance-" sockets as well, since the filename doesn't explain the role, we're using the status call to figure out the use. uuid's are keys in this case
o server_id type to str in kill_session.py so we can match either legacy or mvc sockets
o hook ExportController to OpenVPN model using getInstanceById() to glue the Client Export utility to both components
o extend connection status with mvc sessions (descriptions)

---------

Co-authored-by: Franco Fichtner <franco@opnsense.org>
This commit is contained in:
Ad Schellevis 2023-05-31 16:04:43 +02:00 committed by GitHub
parent f5868fd0f1
commit e9edb11535
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 2076 additions and 192 deletions

View File

@ -1,8 +1,8 @@
<?php
/*
* Copyright (C) 2016-2023 Deciso B.V.
* Copyright (C) 2015-2022 Franco Fichtner <franco@opnsense.org>
* Copyright (C) 2016 Deciso B.V.
* Copyright (C) 2008 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2006 Fernando Lemos
* Copyright (C) 2005 Peter Allgeyer <allgeyer@web.de>
@ -72,32 +72,38 @@ function openvpn_services()
}
}
foreach ((new OPNsense\OpenVPN\OpenVPN())->Instances->Instance->iterateItems() as $key => $node) {
if (!empty((string)$node->enabled)) {
$services[] = [
'description' => "OpenVPN {$node->role} " . htmlspecialchars($node->description),
'pidfile' => "/var/run/ovpn-instance-{$key}.pid",
'configd' => [
'start' => ["openvpn start {$key}"],
'restart' => ["openvpn restart {$key}"],
'stop' => ["openvpn stop {$key}"],
],
'id' => $key,
'name' => "openvpn"
];
}
}
return $services;
}
function openvpn_interfaces()
{
global $config;
$interfaces = array();
foreach (array('server', 'client') as $mode) {
if (isset($config['openvpn']["openvpn-{$mode}"])) {
foreach ($config['openvpn']["openvpn-{$mode}"] as $settings) {
if (empty($settings['disable'])) {
$oic = array('enable' => true);
$oic['if'] = 'openvpn';
$oic['descr'] = 'OpenVPN';
$oic['type'] = 'group';
$oic['virtual'] = true;
$oic['networks'] = array();
$interfaces['openvpn'] = $oic;
break 2;
}
}
}
$interfaces = [];
if ((new OPNsense\OpenVPN\OpenVPN())->isEnabled()) {
$interfaces['openvpn'] = [
'enable' => true,
'if' => 'openvpn',
'descr' => 'OpenVPN',
'type' => 'group',
'virtual' => true,
'networks' => []
];
}
return $interfaces;
}
@ -119,6 +125,16 @@ function openvpn_devices()
}
}
foreach ((new OPNsense\OpenVPN\OpenVPN())->Instances->Instance->iterateItems() as $key => $node) {
$mode = ((string)$node->role)[0];
$name = "ovpn{$mode}{$node->vpnid}";
$names[$name] = [
'descr' => sprintf('ovpn%s%s (OpenVPN %s %s)', $mode, $node->vpnid, (string)$node->role == 'server' ? gettext('Server') : gettext('Client'), $node->description),
'ifdescr' => (string)$node->description,
'name' => $name
];
}
return [[
'function' => 'openvpn_prepare', /* XXX not the same as real configuration */
'configurable' => false,
@ -135,7 +151,7 @@ function openvpn_xmlrpc_sync()
$result[] = array(
'description' => gettext('OpenVPN'),
'section' => 'openvpn',
'section' => 'openvpn,OPNsense.OpenVPN',
'id' => 'openvpn',
'services' => ["openvpn"],
);
@ -194,36 +210,14 @@ function openvpn_create_key()
return $rslt;
}
function openvpn_vpnid_used($vpnid)
{
global $config;
if (isset($config['openvpn']['openvpn-server'])) {
foreach ($config['openvpn']['openvpn-server'] as $settings) {
if ($vpnid == $settings['vpnid']) {
return true;
}
}
}
if (isset($config['openvpn']['openvpn-client'])) {
foreach ($config['openvpn']['openvpn-client'] as $settings) {
if ($vpnid == $settings['vpnid']) {
return true;
}
}
}
return false;
}
function openvpn_vpnid_next()
{
$vpnid = 1;
while (openvpn_vpnid_used($vpnid)) {
$vpnid++;
$vpnids = (new OPNsense\OpenVPN\OpenVPN())->usedVPNIds();
for ($vpnid = 1; true ; $vpnid++) {
if (!in_array($vpnid, $vpnids)) {
return $vpnid;
}
}
return $vpnid;
}
function openvpn_port_used($prot, $interface, $port, $curvpnid = 0)
@ -1043,16 +1037,12 @@ function openvpn_csc_conf_write($settings, $server, $target_filename = null)
function openvpn_prepare($device)
{
global $config;
foreach (['server', 'client'] as $mode) {
if (!empty($config['openvpn']["openvpn-{$mode}"])) {
foreach ($config['openvpn']["openvpn-{$mode}"] as $settings) {
if ($device == "ovpn{$mode[0]}{$settings['vpnid']}") {
openvpn_reconfigure($mode, $settings, true);
return;
}
}
if (str_starts_with($device, 'ovpn')) {
$vpnid = preg_replace("/[^0-9]/", "", $device);
$settings = (new OPNsense\OpenVPN\OpenVPN())->getInstanceById($vpnid);
if ($settings) {
// XXX: split device creation and legacy configure?
openvpn_reconfigure($settings['role'], $settings, true);
}
}
}
@ -1118,34 +1108,6 @@ function openvpn_configure_do($verbose = false, $interface = '', $carp_event = f
}
function openvpn_config()
{
global $config;
$result = [];
foreach (['openvpn-server', 'openvpn-client'] as $section) {
$result[$section] = [];
if (!empty($config['openvpn'][$section])) {
foreach ($config['openvpn'][$section] as $settings) {
if (empty($settings) || isset($settings['disable'])) {
continue;
}
$server = [];
$default_port = ($section == 'openvpn-server') ? 1194 : '';
$server['port'] = ($settings['local_port']) ? $settings['local_port'] : $default_port;
$server['mode'] = $settings['mode'];
if (empty($settings['description'])) {
$settings['description'] = ($section == 'openvpn-server') ? 'Server' : 'Client';
}
$server['name'] = "{$settings['description']} {$settings['protocol']}:{$settings['local_port']}";
$server['vpnid'] = $settings['vpnid'];
$result[$section][] = $server;
}
}
}
return $result;
}
function openvpn_create_dirs()
{
@mkdir('/var/etc/openvpn-csc', 0750);
@ -1306,4 +1268,12 @@ function openvpn_refresh_crls()
}
}
}
foreach ((new OPNsense\OpenVPN\OpenVPN())->Instances->Instance->iterateItems() as $key => $node) {
if (!empty((string)$node->enabled) && !empty((string)$node->crl)) {
$fpath = "/var/etc/openvpn/server-{$key}.crl-verify";
$crl = lookup_crl((string)$node->crl);
file_put_contents($fpath, !empty($crl['text']) ? base64_decode($crl['text']) : '');
@chmod($fpath, 0644);
}
}
}

View File

@ -33,6 +33,7 @@ use OPNsense\Base\UserException;
use OPNsense\Core\Config;
use OPNsense\Core\Backend;
use OPNsense\Trust\Store;
use OPNsense\OpenVPN\OpenVPN;
use OPNsense\OpenVPN\Export;
use OPNsense\OpenVPN\ExportFactory;
@ -94,15 +95,34 @@ class ExportController extends ApiControllerBase
*/
private function openvpnServers($active = true)
{
if (isset(Config::getInstance()->object()->openvpn)) {
foreach (Config::getInstance()->object()->openvpn->children() as $key => $value) {
$cfg = Config::getInstance()->object();
if (isset($cfg->openvpn)) {
foreach ($cfg->openvpn->children() as $key => $server) {
if ($key == 'openvpn-server') {
if (empty($value->disable) || !$active) {
yield $value;
if (empty($server->disable) || !$active) {
$name = empty($server->description) ? "server" : (string)$server->description;
$name .= " " . $server->protocol . ":" . $server->local_port;
yield [
'name' => $name,
'mode' => (string)$server->mode,
'vpnid' => (string)$server->vpnid
];
}
}
}
}
foreach ((new OpenVPN())->Instances->Instance->iterateItems() as $node_uuid => $node){
if (!empty((string)$node->enabled) && $node->role == 'server') {
$name = empty($node->description) ? "server" : (string)$node->description;
$name .= " " . $node->proto . ":" . $node->port;
yield [
'name' => $name,
'mode' => !empty((string)$node->authmode) ? 'server_tls_user' : '',
'vpnid' => $node_uuid
];
}
}
}
/**
@ -132,19 +152,19 @@ class ExportController extends ApiControllerBase
{
$result = array();
$serverModel = $this->getModel()->getServer($vpnid);
$server = $this->findServer($vpnid);
$server = (new OpenVPN())->getInstanceById($vpnid);
// hostname
if (!empty((string)$serverModel->hostname)) {
$result["hostname"] = (string)$serverModel->hostname;
} else {
} elseif (!empty($server['interface'])) {
$allInterfaces = $this->getInterfaces();
if (!empty($allInterfaces[(string)$server->interface])) {
if (strstr((string)$server->protocol, "6") !== false) {
if (!empty($allInterfaces[(string)$server->interface]['ipv6'])) {
$result["hostname"] = $allInterfaces[(string)$server->interface]['ipv6'][0]['ipaddr'];
if (!empty($allInterfaces[$server['interface']])) {
if (strstr($server['protocol'], "6") !== false) {
if (!empty($allInterfaces[$server['interface']]['ipv6'])) {
$result["hostname"] = $allInterfaces[$server['interface']]['ipv6'][0]['ipaddr'];
}
} elseif (!empty($allInterfaces[(string)$server->interface]['ipv4'])) {
$result["hostname"] = $allInterfaces[(string)$server->interface]['ipv4'][0]['ipaddr'];
} elseif (!empty($allInterfaces[$server['interface']]['ipv4'])) {
$result["hostname"] = $allInterfaces[$server['interface']]['ipv4'][0]['ipaddr'];
}
}
}
@ -152,29 +172,13 @@ class ExportController extends ApiControllerBase
foreach ($serverModel->iterateItems() as $field => $value) {
if (!empty((string)$value)) {
$result[$field] = (string)$value;
} elseif (!empty((string)$server->$field) || !isset($result[$field])) {
$result[$field] = (string)$server->$field;
} elseif (!empty($server[$field]) || !isset($result[$field])) {
$result[$field] = $server[$field] ?? null;
}
}
return $result;
}
/**
* find server by vpnid
* @param string $vpnid reference
* @return mixed|null
*/
private function findServer($vpnid)
{
foreach ($this->openvpnServers() as $server) {
if ((string)$server->vpnid == $vpnid) {
return $server;
}
}
return null;
}
/**
* list providers
* @return array list of configured openvpn providers (servers)
@ -184,15 +188,8 @@ class ExportController extends ApiControllerBase
{
$result = array();
foreach ($this->openvpnServers() as $server) {
$vpnid = (string)$server->vpnid;
$result[$vpnid] = array();
// visible name
$result[$vpnid]["name"] = empty($server->description) ? "server" : (string)$server->description;
$result[$vpnid]["name"] .= " " . $server->protocol . ":" . $server->local_port;
// relevant properties
$result[$vpnid]["mode"] = (string)$server->mode;
$result[$vpnid]["vpnid"] = $vpnid;
$result[$vpnid] = array_merge($result[$vpnid], $this->configuredSettings($vpnid));
$vpnid = $server['vpnid'];
$result[$vpnid] = array_merge($server, $this->configuredSettings($vpnid));
}
return $result;
}
@ -210,12 +207,12 @@ class ExportController extends ApiControllerBase
"users" => []
]
];
$server = $this->findServer($vpnid);
$server = (new OpenVPN())->getInstanceById($vpnid);
if ($server !== null) {
// collect certificates for this server's ca
if (isset(Config::getInstance()->object()->cert)) {
foreach (Config::getInstance()->object()->cert as $cert) {
if (isset($cert->refid) && isset($cert->caref) && (string)$server->caref == $cert->caref) {
if (isset($cert->refid) && isset($cert->caref) && $server['caref'] == $cert->caref) {
$result[(string)$cert->refid] = array(
"description" => (string)$cert->descr,
"users" => array()
@ -319,28 +316,16 @@ class ExportController extends ApiControllerBase
{
$response = array("result" => "failed");
if ($this->request->isPost()) {
$server = $this->findServer($vpnid);
$server = (new OpenVPN())->getInstanceById($vpnid);
if ($server !== null) {
// fetch server config data
$config = array();
foreach (
array('disable', 'description', 'local_port', 'protocol', 'crypto', 'digest',
'tunnel_networkv6', 'reneg-sec', 'local_network', 'local_networkv6',
'tunnel_network', 'compression', 'passtos', 'shared_key', 'mode',
'dev_mode', 'tls', 'tlsmode', 'client_mgmt_port') as $field
) {
if (isset($server->$field) && $server->$field !== "") {
$config[$field] = (string)$server->$field;
} else {
$config[$field] = null;
}
}
$config = $server;
// fetch associated certificate data, add to config
$config['server_ca_chain'] = '';
$config['server_subject_name'] = null;
$config['server_cert_is_srv'] = null;
if (!empty($server->certref)) {
$cert = (new Store())->getCertificate((string)$server->certref);
if (!empty($server['certref'])) {
$cert = (new Store())->getCertificate($server['certref']);
if ($cert) {
$config['server_cert_is_srv'] = $cert['is_server'];
$config['server_subject_name'] = $cert['name'] ?? '';

View File

@ -0,0 +1,107 @@
<?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\ApiMutableModelControllerBase;
use OPNsense\Core\Backend;
/**
* Class InstancesController
* @package OPNsense\OpenVPN\Api
*/
class InstancesController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'instance';
protected static $internalModelClass = 'OPNsense\OpenVPN\OpenVPN';
public function searchAction()
{
return $this->searchBase(
'Instances.Instance',
['description', 'role', 'dev_type', 'enabled']
);
}
public function getAction($uuid = null)
{
return $this->getBase('instance', 'Instances.Instance', $uuid);
}
public function addAction()
{
return $this->addBase('instance', 'Instances.Instance');
}
public function setAction($uuid = null)
{
return $this->setBase('instance', 'Instances.Instance', $uuid);
}
public function delAction($uuid)
{
return $this->delBase('Instances.Instance', $uuid);
}
public function toggleAction($uuid, $enabled = null)
{
return $this->toggleBase('Instances.Instance', $uuid, $enabled);
}
/**
* static key administration
*/
public function searchStaticKeyAction()
{
return $this->searchBase('StaticKeys.StaticKey', ['description']);
}
public function getStaticKeyAction($uuid = null)
{
return $this->getBase('statickey', 'StaticKeys.StaticKey', $uuid);
}
public function addStaticKeyAction()
{
return $this->addBase('statickey', 'StaticKeys.StaticKey');
}
public function setStaticKeyAction($uuid = null)
{
return $this->setBase('statickey', 'StaticKeys.StaticKey', $uuid);
}
public function delStaticKeyAction($uuid)
{
return $this->delStaticKeyBase('StaticKeys.StaticKey', $uuid);
}
public function genKeyAction()
{
$key = (new Backend())->configdRun("openvpn genkey");
if (strpos($key, '-----BEGIN') > 0) {
return [
'result' => 'ok',
'key' => trim($key)
];
}
return ['result' => 'failed'];
}
}

View File

@ -31,6 +31,7 @@ namespace OPNsense\OpenVPN\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Config;
use OPNsense\Core\Backend;
use OPNsense\OpenVPN\OpenVPN;
/**
* Class ServiceController
@ -46,10 +47,19 @@ class ServiceController extends ApiControllerBase
if (!empty($config->openvpn->$cnf_section)) {
foreach ($config->openvpn->$cnf_section as $cnf) {
if (!empty((string)$cnf->vpnid)) {
$config_payload[(string)$cnf->vpnid] = $cnf;
$config_payload[(string)$cnf->vpnid] = [
'description' => (string)$cnf->description ?? '',
'enabled' => empty((string)$cnf->disable) ? '1' : '0'
];
}
}
}
foreach ((new OpenVPN())->Instances->Instance->iterateItems() as $node_uuid => $node){
$config_payload[$node_uuid] = [
'enabled' => (string)$node->enabled,
'description' => (string)$node->description
];
}
return $config_payload;
}
@ -79,7 +89,7 @@ class ServiceController extends ApiControllerBase
$stats['connected_since'] = date('Y-m-d H:i:s', $stats['timestamp']);
}
if (!empty($config_payload[$idx])) {
$stats['description'] = (string)$config_payload[$idx]->description ?? '';
$stats['description'] = (string)$config_payload[$idx]['description'];
}
if (!empty($stats['client_list'])) {
foreach ($stats['client_list'] as $client) {
@ -95,12 +105,12 @@ class ServiceController extends ApiControllerBase
}
// add non running enabled servers
foreach ($config_payload as $idx => $cnf) {
if (!in_array($idx, $vpnids) && empty((string)$cnf->disable)) {
if (!in_array($idx, $vpnids) && !empty($cnf['enabled'])) {
$records[] = [
'id' => $idx,
'service_id' => "openvpn/" . $idx,
'type' => $role,
'description' => (string)$cnf->description ?? '',
'description' => $cnf['description'],
];
}
}
@ -189,7 +199,7 @@ class ServiceController extends ApiControllerBase
$this->sessionClose();
(new Backend())-> configdpRun('service start', ['openvpn', $id]);
(new Backend())->configdpRun('service start', ['openvpn', $id]);
return ['result' => 'ok'];
}
@ -206,7 +216,7 @@ class ServiceController extends ApiControllerBase
$this->sessionClose();
(new Backend())-> configdpRun('service stop', ['openvpn', $id]);
(new Backend())->configdpRun('service stop', ['openvpn', $id]);
return ['result' => 'ok'];
}
@ -223,7 +233,24 @@ class ServiceController extends ApiControllerBase
$this->sessionClose();
(new Backend())-> configdpRun('service restart', ['openvpn', $id]);
(new Backend())->configdpRun('service restart', ['openvpn', $id]);
return ['result' => 'ok'];
}
/**
* @param int $id server/client id to restart
* @return array
*/
public function reconfigureAction()
{
if (!$this->request->isPost()) {
return ['result' => 'failed'];
}
$this->sessionClose();
(new Backend())->configdpRun('openvpn configure');
return ['result' => 'ok'];
}

View File

@ -0,0 +1,49 @@
<?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 InstancesController
* @package OPNsense\OpenVPN
*/
class InstancesController extends BaseIndexController
{
/**
* default index page
* @throws \Exception
*/
public function indexAction()
{
$this->view->pick('OPNsense/OpenVPN/instances');
$this->view->formDialogInstance = $this->getForm('dialogInstance');
$this->view->formDialogStaticKey = $this->getForm('dialogStaticKey');
}
}

View File

@ -0,0 +1,333 @@
<fields>
<field>
<type>header</type>
<label>General Settings</label>
</field>
<field>
<id>instance.vpnid</id>
<label>vpnid</label>
<!-- hide id, but push to server-->
<style>role</style>
<type>text</type>
</field>
<field>
<id>instance.role</id>
<label>Role</label>
<type>dropdown</type>
<help>
Define the role of this instance
</help>
</field>
<field>
<id>instance.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
<field>
<id>instance.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
</field>
<field>
<id>instance.proto</id>
<label>Protocol</label>
<type>dropdown</type>
<help>Use protocol for communicating with remote host.</help>
</field>
<field>
<id>instance.port</id>
<label>Port number</label>
<type>text</type>
<help>Portnumber to use, defaults to 1194 when in server role, nobind for clients</help>
</field>
<field>
<id>instance.local</id>
<label>Bind address</label>
<type>text</type>
<help>
Optional IP address for bind. If specified, OpenVPN will bind to this address only.
If unspecified, OpenVPN will bind to all interfaces.
</help>
</field>
<field>
<id>instance.dev_type</id>
<label>Type</label>
<type>dropdown</type>
<advanced>true</advanced>
<help>
Choose the type of tunnel, OSI Layer 3 [tun] is the most common option to route IPv4 or IPv6 traffic,
[tap] offers Ethernet 802.3 (OSI Layer 2) connectivity between hosts and is usually combined with a bridge.
</help>
</field>
<field>
<id>instance.verb</id>
<label>Verbosity</label>
<type>dropdown</type>
<advanced>true</advanced>
<help>Output verbosity level (0..9)</help>
</field>
<field>
<id>instance.maxclients</id>
<label>Concurrent connections</label>
<advanced>true</advanced>
<style>role role_server</style>
<type>text</type>
<help>Specify the maximum number of clients allowed to concurrently connect to this server.</help>
</field>
<field>
<id>instance.keepalive_interval</id>
<label>Keep alive interval</label>
<advanced>true</advanced>
<type>text</type>
<help>Ping interval in seconds. 0 to disable keep alive</help>
</field>
<field>
<id>instance.keepalive_timeout</id>
<label>Keep alive timeout</label>
<advanced>true</advanced>
<type>text</type>
<help>Causes OpenVPN to restart after n seconds pass without reception of a ping or other packet from remote.</help>
</field>
<field>
<id>instance.server</id>
<label>Server (IPv4)</label>
<type>text</type>
<style>role role_server</style>
<help>
This directive will set up an OpenVPN server which will allocate addresses to clients out of the given network/netmask.
The server itself will take the .1 address of the given network for use as the server-side endpoint of the local TUN/TAP interface
</help>
</field>
<field>
<id>instance.server_ipv6</id>
<label>Server (IPv6)</label>
<type>text</type>
<style>role role_server</style>
<help>
This directive will set up an OpenVPN server which will allocate addresses to clients out of the given network/netmask.
The server itself will take the next base address (+1) of the given network for use as the server-side endpoint of the local TUN/TAP interface
</help>
</field>
<field>
<id>instance.topology</id>
<label>Topology</label>
<type>dropdown</type>
<style>selectpicker role role_server</style>
<help>
Configure virtual addressing topology when running in --dev tun mode.
This directive has no meaning in --dev tap mode, which always uses a subnet topology.
</help>
</field>
<field>
<id>instance.remote</id>
<label>Remote</label>
<type>select_multiple</type>
<allownew>true</allownew>
<style>tokenize role role_client</style>
<help>Remote host name or IP address with optional port, examples:
my.remote.local dead:beaf:: my.remote.local:1494 [dead:beaf::]:1494 192.168.1.1:1494
</help>
</field>
<field>
<type>header</type>
<label>Trust</label>
</field>
<field>
<id>instance.cert</id>
<label>Certificate</label>
<type>dropdown</type>
<help>Select a certificate to use for this service.</help>
</field>
<field>
<id>instance.crl</id>
<label>Certificate Revocation List</label>
<type>dropdown</type>
<help>Select a certificate revocation list to use for this service.</help>
</field>
<field>
<id>instance.verify_client_cert</id>
<label>Verify Client Certificate</label>
<type>dropdown</type>
<help>Specify if the client is required to offer a certificate.</help>
</field>
<field>
<id>instance.tls_key</id>
<label>TLS static key</label>
<type>dropdown</type>
<help>
Add an additional layer of HMAC authentication on top of the TLS control channel to mitigate DoS attacks and attacks on the TLS stack.
The prefixed mode determines if this measurement is only used for authentication (--tls-auth) or includes encryption (--tls-crypt).
</help>
</field>
<field>
<id>instance.data-ciphers</id>
<label>Data Ciphers</label>
<type>select_multiple</type>
<advanced>true</advanced>
<style>selectpicker role role_server</style>
<help>Restrict the allowed ciphers to be negotiated to the ciphers in this list.</help>
</field>
<field>
<id>instance.data-ciphers-fallback</id>
<label>Data Ciphers Fallback</label>
<type>dropdown</type>
<advanced>true</advanced>
<style>selectpicker role role_server</style>
<help>
Configure a cipher that is used to fall back to if we could not determine which cipher the peer is willing to use.
This option should only be needed to connect to peers that are running OpenVPN 2.3 or older versions,
and have been configured with --enable-small (typically used on routers or other embedded devices).
</help>
</field>
<field>
<type>header</type>
<label>Authentication</label>
</field>
<field>
<id>instance.authmode</id>
<label>Authentication</label>
<type>select_multiple</type>
<style>selectpicker role role_server</style>
<help>Select authentication methods to use, leave empty if no challenge response authentication is needed.</help>
</field>
<field>
<id>instance.local_group</id>
<label>Enforce local group</label>
<type>dropdown</type>
<style>selectpicker role role_server</style>
<help>Restrict access to users in the selected local group. Please be aware that other authentication backends will refuse to authenticate when using this option.</help>
</field>
<field>
<id>instance.username_as_common_name</id>
<label>Username as CN</label>
<type>checkbox</type>
<advanced>true</advanced>
<style>role role_server</style>
<help>Use the authenticated username as the common-name, rather than the common-name from the client certificate.</help>
</field>
<field>
<id>instance.strictusercn</id>
<label>Strict User/CN Matching</label>
<type>checkbox</type>
<style>role role_server</style>
<help>When authenticating users, enforce a match between the Common Name of the client certificate and the username given at login.</help>
</field>
<field>
<id>instance.username</id>
<label>Username</label>
<type>text</type>
<style>role role_client</style>
<help>(optional) Username to send to the server for authentication when required.</help>
</field>
<field>
<id>instance.password</id>
<label>Password</label>
<type>password</type>
<style>role role_client</style>
<help>Password belonging to the user specified above</help>
</field>
<field>
<id>instance.reneg-sec</id>
<label>Renegotiate time</label>
<type>text</type>
<help>Renegotiate data channel key after n seconds (default=3600).
When using a one time password, be advised that your connection will automatically drop because your password is not valid anymore.
Set to 0 to disable, remember to change your client as well.
</help>
</field>
<field>
<id>instance.auth-gen-token</id>
<label>Auth Token Lifetime</label>
<type>text</type>
<style>role role_server</style>
<help>After successful user/password authentication,
the OpenVPN server will with this option generate a temporary authentication token and push that to the client.
On the following renegotiations, the OpenVPN client will pass this token instead of the users password.
On the server side the server will do the token authentication internally and it will
NOT do any additional authentications against configured external user/password authentication mechanisms.
When set to 0, the token will never expire, any other value specifies the lifetime in seconds.
</help>
</field>
<field>
<type>header</type>
<label>Routing</label>
</field>
<field>
<id>instance.push_route</id>
<label>Local Network</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>These are the networks accessible on this host, these are pushed via route{-ipv6} clauses in OpenVPN to the client.</help>
</field>
<field>
<id>instance.route</id>
<label>Remote Network</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>Remote networks for the server, add route to routing table after connection is established</help>
</field>
<field>
<type>header</type>
<label>Miscellaneous</label>
</field>
<field>
<id>instance.various_flags</id>
<label>Options</label>
<type>select_multiple</type>
<help>Various less frequently used yes/no options which can be set for this instance.</help>
</field>
<field>
<id>instance.redirect_gateway</id>
<label>Redirect gateway</label>
<type>select_multiple</type>
<style>selectpicker role role_server</style>
<help>Automatically execute routing commands to cause all outgoing IP traffic to be redirected over the VPN.</help>
</field>
<field>
<id>instance.register_dns</id>
<label>Register DNS</label>
<type>checkbox</type>
<style>role role_server</style>
<help>Run ipconfig /flushdns and ipconfig /registerdns on connection initiation. This is known to kick Windows into recognizing pushed DNS servers.</help>
</field>
<field>
<id>instance.dns_domain</id>
<label>DNS Default Domain</label>
<type>text</type>
<style>role role_server</style>
<help>Set Connection-specific DNS Suffix.</help>
</field>
<field>
<id>instance.dns_domain_search</id>
<label>DNS Domain search list</label>
<type>select_multiple</type>
<style>tokenize role role_server</style>
<allownew>true</allownew>
<help>
Add name to the domain search list. Repeat this option to add more entries. Up to 10 domains are supported
</help>
</field>
<field>
<id>instance.dns_servers</id>
<label>DNS Servers</label>
<type>select_multiple</type>
<style>tokenize role role_server</style>
<allownew>true</allownew>
<help>
Set primary domain name server IPv4 or IPv6 address. Repeat this option to set secondary DNS server addresses.
</help>
</field>
<field>
<id>instance.ntp_servers</id>
<label>NTP Servers</label>
<type>select_multiple</type>
<style>tokenize role role_server</style>
<allownew>true</allownew>
<help>
Set primary NTP server address (Network Time Protocol). Repeat this option to set secondary NTP server addresses.
</help>
</field>
</fields>

View File

@ -0,0 +1,21 @@
<fields>
<field>
<id>statickey.description</id>
<label>Description</label>
<type>text</type>
<help>You may enter a description here for your reference (not parsed).</help>
</field>
<field>
<id>statickey.mode</id>
<label>Mode</label>
<width>220px</width>
<type>dropdown</type>
<help>Define the use of this key, authentication (--tls-auth) or authentication and encryption (--tls-crypt)</help>
</field>
<field>
<id>statickey.key</id>
<label>Static Key</label>
<type>textbox</type>
<help>Paste an OpenVPN Static key. Or generate one with the button.</help>
</field>
</fields>

View File

@ -747,6 +747,13 @@
<pattern>api/openvpn/client_overwrites/*</pattern>
</patterns>
</page-openvpn-csc>
<page-openvpn-instances>
<name>VPN: OpenVPN: Instances</name>
<patterns>
<pattern>ui/openvpn/instances</pattern>
<pattern>api/openvpn/instances/*</pattern>
</patterns>
</page-openvpn-instances>
<page-openvpn-server>
<name>VPN: OpenVPN: Server</name>
<patterns>

View File

@ -204,6 +204,7 @@
</Services>
<VPN order="50" cssClass="fa fa-globe">
<OpenVPN cssClass="fa fa-lock fa-fw" order="20">
<Instances VisibleName="Instances [new]" order="5" url="/ui/openvpn/instances"/>
<Servers order="10" url="/vpn_openvpn_server.php">
<Edit url="/vpn_openvpn_server.php?*" visibility="hidden"/>
<Step url="/wizard.php?xml=openvpn*" visibility="hidden"/>

View File

@ -5,7 +5,7 @@
<items>
<servers>
<server type="ArrayField">
<vpnid type="IntegerField">
<vpnid type="TextField">
<Required>Y</Required>
</vpnid>
<hostname type="HostnameField">

View File

@ -0,0 +1,63 @@
<?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\FieldTypes;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\TextField;
class InstanceField extends ArrayField
{
/**
* push internal reusable properties as virtuals
*/
protected function actionPostLoadingEvent()
{
foreach ($this->internalChildnodes as $node) {
$uuid = $node->getAttributes()['uuid'] ?? null;
if (!$node->getInternalIsVirtual() && $uuid) {
$files = [
'cnfFilename' => "/var/etc/openvpn/instance-{$uuid}.conf",
'pidFilename' => "/var/run/ovpn-instance-{$uuid}.pid",
'sockFilename' => "/var/etc/openvpn/instance-{$uuid}.sock",
'statFilename' => "/var/etc/openvpn/instance-{$uuid}.stat",
'csoDirectory' => "/var/etc/openvpn-csc/$node->vpnid",
'__devnode' => "{$node->dev_type}{$node->vpnid}",
'__devname' => "ovpn".((string)$node->role)[0]."{$node->vpnid}",
];
foreach ($files as $name => $payload) {
$new_item = new TextField();
$new_item->setInternalIsVirtual();
$new_item->setValue($payload);
$node->addChildNode($name, $new_item);
}
}
}
return parent::actionPostLoadingEvent();
}
}

View File

@ -0,0 +1,95 @@
<?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\FieldTypes;
use OPNsense\Base\FieldTypes\BaseField;
use OPNsense\Base\Validators\CallbackValidator;
/**
* @package OPNsense\Base\FieldTypes
*/
class RemoteHostField extends BaseField
{
protected $internalIsContainer = false;
/**
* get valid options, descriptions and selected value
* @return array
*/
public function getNodeData()
{
$result = [];
foreach (explode(',', $this->internalValue) as $opt) {
$result[$opt] = array("value" => $opt, "selected" => 1);
}
return $result;
}
/**
* @inheritdoc
*/
public function getValidators()
{
$validators = parent::getValidators();
if ($this->internalValue != null) {
$validators[] = new CallbackValidator(
[
"callback" => function ($value) {
$errors = [];
foreach (explode(',', $value) as $this_remote) {
$parts = [];
if (substr_count($this_remote, ':') > 1) {
foreach (explode(']', $this_remote) as $part) {
$parts[] = ltrim($part, '[:');
}
} else {
$parts = explode(':', $this_remote);
}
if (
filter_var($parts[0], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false &&
filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false
) {
$errors[] = sprintf(gettext("hostname %s is not a valid hostname."), $parts[0]);
} elseif (
isset($parts[1]) &&
filter_var($parts[1], FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'max_range' => 65535]]
) === false
) {
$errors[] = sprintf(gettext("port %s not valid."), $parts[1]);
}
}
return $errors;
}
]
);
}
return $validators;
}
}

View File

@ -0,0 +1,97 @@
<?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\FieldTypes;
use OPNsense\Base\Validators\CallbackValidator;
use OPNsense\Base\FieldTypes\IntegerField;
/**
* @package OPNsense\Base\FieldTypes
*/
class VPNIdField extends IntegerField
{
private static $internalLegacyVPNids = [];
/**
* fetch (legacy) vpn id's as these are reserved
*/
protected function actionPostLoadingEvent()
{
if (empty(self::$internalLegacyVPNids)) {
self::$internalLegacyVPNids = $this->getParentModel()->usedVPNIds();
}
}
/**
* {@inheritdoc}
*/
public function setValue($value)
{
if ($value == '') {
// enforce default when not set
for ($i = 1; true ; $i++) {
if (!in_array($i, self::$internalLegacyVPNids)) {
$this->internalValue = (string)$i;
$this_uuid = $this->getParentNode()->getAttributes()['uuid'];
self::$internalLegacyVPNids[$this_uuid] = $i;
break;
}
}
} else {
parent::setValue($value);
}
}
/**
* retrieve field validators for this field type
* @return array returns list of validators
*/
public function getValidators()
{
$validators = parent::getValidators();
$vpnids = self::$internalLegacyVPNids;
$this_uuid = $this->getParentNode()->getAttributes()['uuid'];
$validators[] = new CallbackValidator(
[
"callback" => function ($value) use ($vpnids, $this_uuid) {
foreach ($vpnids as $key => $vpnid) {
if ($vpnid == $value && $key != $this_uuid) {
return [gettext('Value should be unique')];
}
}
return [];
}
]
);
return $validators;
}
}

View File

@ -28,7 +28,12 @@
namespace OPNsense\OpenVPN;
use Phalcon\Messages\Message;
use OPNsense\Base\BaseModel;
use OPNsense\Trust\Store;
use OPNsense\Core\Config;
use OPNsense\Firewall\Util;
/**
* Class OpenVPN
@ -36,6 +41,82 @@ use OPNsense\Base\BaseModel;
*/
class OpenVPN extends BaseModel
{
/**
* {@inheritdoc}
*/
public function performValidation($validateFullModel = false)
{
$messages = parent::performValidation($validateFullModel);
$instances = [];
foreach ($this->getFlatNodes() as $key => $node) {
if ($validateFullModel || $node->isFieldChanged()) {
$tagName = $node->getInternalXMLTagName();
$parentNode = $node->getParentNode();
$parentKey = $parentNode->__reference;
$parentTagName = $parentNode->getInternalXMLTagName();
if ($parentTagName === 'Instance') {
$instances[$parentKey] = $parentNode;
}
}
}
// validate changed instances
foreach ($instances as $key => $instance) {
if ($instance->role == 'client') {
if (empty((string)$instance->remote)) {
$messages->appendMessage(new Message(gettext("Remote required"), $key . ".remote"));
}
if (empty((string)$instance->username) xor empty((string)$instance->password)) {
$messages->appendMessage(
new Message(
gettext("When ussing password authentication, both username and password are required"),
$key . ".username"
)
);
}
}
if (!empty((string)$instance->cert)) {
if ($instance->cert->isFieldChanged() || $validateFullModel) {
$tmp = Store::getCertificate((string)$instance->cert);
if (empty($tmp) || !isset($tmp['ca'])) {
$messages->appendMessage(new Message(
gettext("Unable to locate a Certificate Authority for this certificate"), $key . ".cert"
));
}
}
} else {
if (
$instance->cert->isFieldChanged() ||
$instance->verify_client_cert->isFieldChanged() ||
$validateFullModel
) {
if ((string)$node->verify_client_cert != 'none') {
$messages->appendMessage(new Message(
gettext("To validate a certificate, one has to be provided "), $key . ".verify_client_cert"
));
}
}
}
if ((
$instance->keepalive_interval->isFieldChanged() ||
$instance->keepalive_timeout->isFieldChanged() ||
$validateFullModel
) && (int)(string)$instance->keepalive_timeout < (int)(string)$instance->keepalive_interval
) {
$messages->appendMessage(new Message(
gettext("Timeout should be larger than interval"), $key . ".keepalive_timeout"
));
}
}
return $messages;
}
/**
* Retrieve overwrite content in legacy format
* @param string $server_id vpnid
* @param string $common_name certificate common name (or username when specified)
* @return array legacy overwrite data
*/
public function getOverwrite($server_id, $common_name)
{
$result = [];
@ -92,4 +173,368 @@ class OpenVPN extends BaseModel
}
return $result;
}
/**
* The VPNid sequence is used for device creation, in which case we can't use uuid's due to their size
* @return list of vpn id's used by legacy or mvc instances
*/
public function usedVPNIds()
{
$result = [];
$cfg = Config::getInstance()->object();
foreach (['openvpn-server', 'openvpn-client'] as $ref) {
if (isset($cfg->openvpn) && isset($cfg->openvpn->$ref)) {
foreach ($cfg->openvpn->$ref as $item) {
if (isset($item->vpnid)) {
$result[] = (string)$item->vpnid;
}
}
}
}
foreach ($this->Instances->Instance->iterateItems() as $node_uuid => $node){
if ((string)$node->vpnid != '') {
$result[$node_uuid] = (string)$node->vpnid;
}
}
return $result;
}
/**
* @return bool true when there is any enabled tunnel (legacy and/or mvc)
*/
public function isEnabled()
{
$cfg = Config::getInstance()->object();
foreach (['openvpn-server', 'openvpn-client'] as $ref) {
if (isset($cfg->openvpn) && isset($cfg->openvpn->$ref)) {
foreach ($cfg->openvpn->$ref as $item) {
if (empty((string)$item->disable)) {
return true;
}
}
}
}
foreach ($this->Instances->Instance->iterateItems() as $node_uuid => $node){
if (!empty((string)$node->enabled)) {
return true;
}
}
return false;
}
/**
* Find unique instance properties, either from legacy or mvc model
* Offers glue between both worlds.
* @param string $server_id vpnid (either numerical or uuid)
* @return array selection of relevant fields for downstream processes
*/
public function getInstanceById($server_id, $role=null)
{
// travers model first, two key types are valid, the id used in the device (numeric) or the uuid
foreach ($this->Instances->Instance->iterateItems() as $node_uuid => $node){
if (
!empty((string)$node->enabled) &&
((string)$node->vpnid == $server_id || $server_id == $node_uuid) &&
($role == null || $role == (string)$node->role)
) {
// find static key
$this_tls = null;
$this_mode = null;
if (!empty((string)$node->tls_key)) {
$tlsnode = $this->getNodeByReference("StaticKeys.StaticKey.{$node->tls_key}");
if (!empty($node->tls_key)) {
$this_mode = (string)$tlsnode->mode;
$this_tls = base64_encode((string)$tlsnode->key);
}
}
// find caref
$this_caref = null;
if (isset(Config::getInstance()->object()->cert)) {
foreach (Config::getInstance()->object()->cert as $cert) {
if (isset($cert->refid) && (string)$node->cert == $cert->refid) {
$this_caref = (string)$cert->caref;
}
}
}
return [
'role' => (string)$node->role,
'vpnid' => $server_id,
'authmode' => (string)$node->authmode,
'local_group' => (string)$node->local_group,
'strictusercn' => (string)$node->strictusercn,
'dev_mode' => (string)$node->dev_type,
'topology_subnet' => $node->topology == 'subnet' ? '1' : '0',
'local_port' => (string)$node->port,
'protocol' => (string)$node->proto,
'mode' => !empty((string)$node->authmode) ? 'server_tls_user' : '',
'reneg-sec' => (string)$node->{'reneg-sec'},
'tls' => $this_tls,
'tlsmode' => $this_mode,
'certref' => (string)$node->cert,
'caref' => $this_caref,
'description' => (string)$node->description
];
}
}
// when not found, try to locate the server in our legacy pool
$cfg = Config::getInstance()->object();
foreach (['openvpn-server', 'openvpn-client'] as $section) {
if (!isset($cfg->openvpn) || !isset($cfg->openvpn->$section)) {
continue;
}
foreach ($cfg->openvpn->$section as $item) {
$this_role = explode('-', $section)[1];
// XXX: previous legacy code did not check if the instance is enabled, we might want to revise that
if (
isset($item->vpnid) &&
$item->vpnid == $server_id &&
($role == null || $role == $this_role)
) {
return [
'role' => $this_role,
'vpnid' => (string)$item->vpnid,
'authmode' => (string)$item->authmode,
'local_group' => (string)$item->local_group,
'cso_login_matching' => (string)$item->username_as_common_name,
'strictusercn' => (string)$item->strictusercn,
'dev_mode' => (string)$item->dev_mode,
'topology_subnet' => (string)$item->topology_subnet,
'local_port' => (string)$item->local_port,
'protocol' => (string)$item->protocol,
'mode' => (string)$item->local_port,
'reneg-sec' => (string)$item->{'reneg-sec'},
'tls' => (string)$item->tls,
'tlsmode' => (string)$item->tlsmode,
'certref' => (string)$item->certref,
'caref' => (string)$item->caref,
'description' => (string)$item->description,
// legacy only (backwards compatibility)
'compression' => (string)$item->compression,
'crypto' => (string)$item->crypto,
'digest' => (string)$item->digest,
'interface' => (string)$item->interface,
];
}
}
}
return null;
}
/**
* Convert options into a openvpn config file on disk
* @param string $filename target filename
* @return null
*/
private function writeConfig($filename, $options)
{
$output = '';
foreach ($options as $key => $value) {
if ($value === null) {
$output .= $key . "\n";
} elseif (str_starts_with($key, '<')) {
$output .= $key ."\n";
$output .= trim($value)."\n";
$output .= "</" . substr($key, 1) . "\n";
} elseif (is_array($value)) {
if ($key == 'auth-user-pass') {
// user/passwords need to be feed using a file
$output .= $key . " ". $value['filename'] . "\n";
file_put_contents($value['filename'], $value['content']);
@chmod($value['filename'], 0600);
} else {
foreach ($value as $item) {
$output .= $key . " " . $item ."\n";
}
}
} else {
$output .= $key . " " . $value ."\n";
}
}
file_put_contents($filename, $output);
@chmod($filename, 0600);
}
/**
* generate OpenVPN instance config files.
* Ideally we would like to use our standard template system, but due to the complexity of the output
* and the need for multiple files and a cleanup, this would add more unwanted complexity.
*/
public function generateInstanceConfig($uuid=null)
{
foreach ($this->Instances->Instance->iterateItems() as $node_uuid => $node){
if (!empty((string)$node->enabled) && ($uuid == null || $node_uuid == $uuid)) {
$options = ['push' => [], 'route' => [], 'route-ipv6' => []];
// mode specific settings
if ($node->role == 'client') {
$options['client'] = null;
$options['dev'] = "ovpnc{$node->vpnid}";
$options['remote'] = [];
foreach (explode(',', (string)$node->remote) as $this_remote) {
$parts = [];
if (substr_count($this_remote, ':') > 1) {
foreach (explode(']', $this_remote) as $part) {
$parts[] = ltrim($part, '[:');
}
} else {
$parts = explode(':', $this_remote);
}
$options['remote'][] = implode(' ', $parts);
}
if (empty((string)$node->port) && empty((string)$node->local)) {
$options['nobind'] = null;
}
if (!empty((string)$node->username) && !empty((string)$node->password)) {
$options['auth-user-pass'] = [
"filename" => "/var/etc/openvpn/instance-{$node_uuid}.up",
"content" => "{$node->username}\n{$node->password}\n"
];
}
// XXX: In some cases it might be practical to drop privileges, for server mode this will be
// more difficult due to the associated script actions (and their requirements).
//$options['user'] = 'openvpn';
//$options['group'] = 'openvpn';
} else {
$event_script = '/usr/local/opnsense/scripts/openvpn/ovpn_event.py';
$options['dev'] = "ovpns{$node->vpnid}";
$options['ping-timer-rem'] = null;
$options['topology'] = (string)$node->topology;
$options['dh'] = '/usr/local/etc/inc/plugins.inc.d/openvpn/dh.rfc7919';
if (!empty((string)$node->crl) && !empty((string)$node->cert)) {
// updated via plugins_configure('crl');
$options['crl-verify'] = "/var/etc/openvpn/server-{$node_uuid}.crl-verify";
}
if (!empty((string)$node->server)) {
$parts = explode('/', (string)$node->server);
$options['server'] = $parts[0] . " " . Util::CIDRToMask($parts[1]);
}
if (!empty((string)$node->server_ipv6)) {
$options['server-ipv6'] = (string)$node->server_ipv6;
}
if (!empty((string)$node->username_as_common_name)) {
$options['username-as-common-name'] = null;
}
// server only setttings
if (!empty((string)$node->server) || !empty((string)$node->server_ipv6)) {
$options['client-config-dir'] = "/var/etc/openvpn-csc/{$node->vpnid}";
// hook event handlers
if (!empty((string)$node->authmode)) {
$options['auth-user-pass-verify'] = "\"{$event_script} --defer '{$node->vpnid}'\" via-env";
$options['learn-address'] = "\"{$event_script} '{$node->vpnid}'\"";
} else {
// client specific profiles are being deployed using the connect event when no auth is used
$options['client-connect'] = "\"{$event_script} '{$node->vpnid}'\"";
}
$options['client-disconnect'] = "\"{$event_script} '{$node->vpnid}'\"";
$options['tls-verify'] = "\"{$event_script} '{$node->vpnid}'\"";
}
if (!empty((string)$node->maxclients)) {
$options['max-clients'] = (string)$node->maxclients;
}
if (empty((string)$node->local) && str_starts_with((string)$node->proto, 'udp')) {
// assume multihome when no bind address is specified for udp
$options['multihome'] = null;
}
// push options
if (!empty((string)$node->redirect_gateway)) {
$options['push'][] = "\"redirect-gateway {$node->redirect_gateway}\"";
}
if (!empty((string)$node->register_dns)) {
$options['push'][] = "\"register-dns\"";
}
if (!empty((string)$node->dns_domain)) {
$options['push'][] = "\"dhcp-option DOMAIN {$node->dns_domain}\"";
}
if (!empty((string)$node->dns_domain_search)) {
foreach (explode(',', (string)$node->dns_domain_search) as $opt) {
$options['push'][] = "\"dhcp-option DOMAIN-SEARCH {$opt}\"";
}
}
if (!empty((string)$node->dns_servers)) {
foreach (explode(',', (string)$node->dns_servers) as $opt) {
$options['push'][] = "\"dhcp-option DNS {$opt}\"";
}
}
if (!empty((string)$node->ntp_servers)) {
foreach (explode(',', (string)$node->ntp_servers) as $opt) {
$options['push'][] = "\"dhcp-option NTP {$opt}\"";
}
}
}
$options['persist-tun'] = null;
$options['persist-key'] = null;
if (!empty((string)$node->keepalive_interval) && !empty((string)$node->keepalive_timeout)) {
$options['keepalive'] = "{$node->keepalive_interval} {$node->keepalive_timeout}";
}
$options['dev-type'] = (string)$node->dev_type;
$options['dev-node'] = "/dev/{$node->dev_type}{$node->vpnid}";
$options['script-security'] = '3';
$options['writepid'] = $node->pidFilename;
$options['daemon'] = "openvpn_{$node->role}{$node->vpnid}";
$options['management'] = "{$node->sockFilename} unix";
$options['proto'] = (string)$node->proto;
$options['verb'] = (string)$node->verb;
$options['verify-client-cert'] = (string)$node->verify_client_cert;
foreach (
['reneg-sec', 'auth-gen-token', 'port', 'local', 'data-ciphers', 'data-ciphers-fallback'] as $opt
) {
if ((string)$node->$opt != '') {
$options[$opt] = str_replace(',', ':', (string)$node->$opt);
}
}
if (!empty((string)$node->various_flags)) {
foreach (explode(',', (string)$node->various_flags) as $opt) {
$options[$opt] = null;
}
}
// routes (ipv4, ipv6 local or push)
foreach (['route', 'push_route'] as $type) {
foreach (explode(',', (string)$node->$type) as $item) {
if (empty($item)) {
continue;
} elseif (strpos($item, ":") === false) {
$parts = explode('/', (string)$item);
$item = $parts[0] . " " . Util::CIDRToMask($parts[1] ?? '32');
$target_fieldname = "route";
} else {
$target_fieldname = "route-ipv6";
}
if ($type == 'push_route') {
$options['push'][] = "\"{$target_fieldname} $item\"";
} else {
$options[$target_fieldname][] = $item;
}
}
}
if (!empty((string)$node->tls_key)) {
$tlsnode = $this->getNodeByReference("StaticKeys.StaticKey.{$node->tls_key}");
if ($tlsnode) {
$options["<tls-{$tlsnode->mode}>"] = (string)$tlsnode->key;
if ($tlsnode->mode == 'auth') {
$options['key-direction'] = $node->role == 'server' ? '0' : '1';
}
}
}
if (!empty((string)$node->cert)) {
$tmp = Store::getCertificate((string)$node->cert);
if ($tmp && isset($tmp['prv'])) {
$options['<key>'] = $tmp['prv'];
$options['<cert>'] = $tmp['crt'];
if (isset($tmp['ca'])) {
$options['<ca>'] = $tmp['ca']['crt'];
}
}
}
// dump to file
$this->writeConfig($node->cnfFilename, $options);
}
}
}
}

View File

@ -93,5 +93,265 @@
</description>
</Overwrite>
</Overwrites>
<Instances>
<Instance type=".\InstanceField">
<vpnid type=".\VPNIdField">
<Required>Y</Required>
</vpnid>
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<dev_type type="OptionField">
<Required>Y</Required>
<default>tun</default>
<OptionValues>
<tun>tun</tun>
<tap>tap</tap>
</OptionValues>
</dev_type>
<verb type="OptionField">
<Required>Y</Required>
<default>3</default>
<OptionValues>
<o0 value="0">0 (No output except fatal errors.)</o0>
<o1 value="1">1 (Normal)</o1>
<o2 value="2">2 (Normal)</o2>
<o3 value="3">3 (Normal)</o3>
<o4 value="4">4 (Normal)</o4>
<o5 value="5">5 (log packets)</o5>
<o6 value="6">6 (debug)</o6>
<o7 value="7">7 (debug)</o7>
<o8 value="8">8 (debug)</o8>
<o9 value="9">9 (debug)</o9>
<o10 value="10">10 (debug)</o10>
<o11 value="11">11 (debug)</o11>
</OptionValues>
</verb>
<proto type="OptionField">
<Required>Y</Required>
<default>udp</default>
<OptionValues>
<udp>UDP</udp>
<udp4>UDP (IPv4)</udp4>
<udp6>UDP (IPv6)</udp6>
<tcp>TCP</tcp>
<tcp4>TCP (IPv4)</tcp4>
<tcp6>TCP (IPv6)</tcp6>
</OptionValues>
</proto>
<port type="PortField">
</port>
<local type="NetworkField">
<Required>N</Required>
<WildcardEnabled>N</WildcardEnabled>
<NetMaskAllowed>N</NetMaskAllowed>
</local>
<topology type="OptionField">
<Required>Y</Required>
<default>subnet</default>
<OptionValues>
<net30>net30</net30>
<p2p>p2p</p2p>
<subnet>subnet</subnet>
</OptionValues>
</topology>
<remote type=".\RemoteHostField">
</remote>
<role type="OptionField">
<default>server</default>
<Required>Y</Required>
<OptionValues>
<client>Client</client>
<server>Server</server>
</OptionValues>
</role>
<server type="NetworkField">
<Required>N</Required>
<WildcardEnabled>N</WildcardEnabled>
</server>
<server_ipv6 type="NetworkField">
<Required>N</Required>
<WildcardEnabled>N</WildcardEnabled>
</server_ipv6>
<route type="NetworkField">
<FieldSeparator>,</FieldSeparator>
<Required>N</Required>
<asList>Y</asList>
<WildcardEnabled>N</WildcardEnabled>
</route>
<push_route type="NetworkField">
<FieldSeparator>,</FieldSeparator>
<Required>N</Required>
<asList>Y</asList>
<WildcardEnabled>N</WildcardEnabled>
</push_route>
<cert type="CertificateField">
<Required>N</Required>
<BlankDesc>None</BlankDesc>
<ValidationMessage>Please select a valid certificate from the list</ValidationMessage>
</cert>
<crl type="CertificateField">
<Required>N</Required>
<type>crl</type>
<BlankDesc>None</BlankDesc>
<ValidationMessage>Please select a valid certificate from the list</ValidationMessage>
</crl>
<verify_client_cert type="OptionField">
<Required>Y</Required>
<default>require</default>
<OptionValues>
<none>none</none>
<require>required</require>
</OptionValues>
</verify_client_cert>
<data-ciphers type="OptionField">
<Required>N</Required>
<multiple>Y</multiple>
<OptionValues>
<AES-256-GCM>AES-256-GCM</AES-256-GCM>
<AES-128-GCM>AES-128-GCM</AES-128-GCM>
<CHACHA20-POLY1305>CHACHA20-POLY1305</CHACHA20-POLY1305>
</OptionValues>
</data-ciphers>
<data-ciphers-fallback type="OptionField">
<Required>N</Required>
<OptionValues>
<AES-256-GCM>AES-256-GCM</AES-256-GCM>
<AES-128-GCM>AES-128-GCM</AES-128-GCM>
<CHACHA20-POLY1305>CHACHA20-POLY1305</CHACHA20-POLY1305>
</OptionValues>
</data-ciphers-fallback>
<tls_key type="ModelRelationField">
<Model>
<host>
<source>OPNsense.OpenVPN.OpenVPN</source>
<items>StaticKeys.StaticKey</items>
<display>mode,description</display>
<display_format>[%s] %s</display_format>
</host>
</Model>
<Required>N</Required>
</tls_key>
<authmode type="AuthenticationServerField">
<Required>N</Required>
<multiple>Y</multiple>
<BlankDesc>None</BlankDesc>
<default>Local Database</default>
</authmode>
<local_group type="AuthGroupField">
<Required>N</Required>
</local_group>
<various_flags type="OptionField">
<Required>N</Required>
<multiple>Y</multiple>
<OptionValues>
<client-to-client>client-to-client</client-to-client>
<duplicate-cn>duplicate-cn</duplicate-cn>
<passtos>passtos</passtos>
<persist-remote-ip>persist-remote-ip</persist-remote-ip>
<route-nopull>route-nopull</route-nopull>
<route-noexec>route-noexec</route-noexec>
<remote-random>remote-random</remote-random>
</OptionValues>
</various_flags>
<username_as_common_name type="BooleanField">
<default>0</default>
<Required>Y</Required>
</username_as_common_name>
<strictusercn type="BooleanField">
<default>0</default>
<Required>Y</Required>
</strictusercn>
<username type="TextField">
</username>
<password type="TextField">
</password>
<maxclients type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>65535</MaximumValue>
</maxclients>
<keepalive_interval type="IntegerField">
<default>10</default>
<MinimumValue>0</MinimumValue>
<MaximumValue>65535</MaximumValue>
</keepalive_interval>
<keepalive_timeout type="IntegerField">
<default>60</default>
<MinimumValue>0</MinimumValue>
<MaximumValue>65535</MaximumValue>
</keepalive_timeout>
<reneg-sec type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>N</Required>
</reneg-sec>
<auth-gen-token type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>N</Required>
</auth-gen-token>
<redirect_gateway type="OptionField">
<Required>N</Required>
<Multiple>Y</Multiple>
<OptionValues>
<local value="local">local</local>
<autolocal value="autolocal">autolocal</autolocal>
<def1>default</def1>
<bypass_dhcp value="bypass-dhcp">bypass dhcp</bypass_dhcp>
<bypass_dns value="bypass-dns">bypass dns</bypass_dns>
<block_local value="block-local">block local</block_local>
<ipv6 value="ipv6">ipv6 (default)</ipv6>
<notipv4 value="!ipv4">not ipv4 (default)</notipv4>
</OptionValues>
</redirect_gateway>
<register_dns type="BooleanField">
<default>0</default>
<Required>Y</Required>
</register_dns>
<dns_domain type="HostnameField">
<Required>N</Required>
</dns_domain>
<dns_domain_search type="HostnameField">
<Required>N</Required>
<FieldSeparator>,</FieldSeparator>
<AsList>Y</AsList>
</dns_domain_search>
<dns_servers type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<Required>N</Required>
<FieldSeparator>,</FieldSeparator>
<asList>Y</asList>
</dns_servers>
<ntp_servers type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<Required>N</Required>
<FieldSeparator>,</FieldSeparator>
<asList>Y</asList>
</ntp_servers>
<description type="TextField">
<Required>N</Required>
</description>
</Instance>
</Instances>
<StaticKeys>
<StaticKey type="ArrayField">
<mode type="OptionField">
<Required>Y</Required>
<default>crypt</default>
<OptionValues>
<auth>auth (Authenticate control channel packets)</auth>
<crypt>crypt (Encrypt and authenticate all control channel packets)</crypt>
</OptionValues>
</mode>
<key type="TextField">
<Required>Y</Required>
<ValidationMessage>A key is required, generate one with the button</ValidationMessage>
</key>
<description type="TextField">
<Required>N</Required>
</description>
</StaticKey>
</StaticKeys>
</items>
</model>

View File

@ -0,0 +1,171 @@
{#
# 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.
#}
<script>
'use strict';
$( document ).ready(function () {
let grid_instances = $("#grid-instances").UIBootgrid({
search:'/api/openvpn/instances/search/',
get:'/api/openvpn/instances/get/',
add:'/api/openvpn/instances/add/',
set:'/api/openvpn/instances/set/',
del:'/api/openvpn/instances/del/',
toggle:'/api/openvpn/instances/toggle/',
options:{
selection: false,
formatters:{
tunnel: function (column, row) {
let items = [];
if (row.tunnel_network) {
items.push(row.tunnel_network);
}
if (row.tunnel_networkv6) {
items.push(row.tunnel_networkv6);
}
return items.join('<br/>');
}
}
}
});
let grid_statickeys = $("#grid-statickeys").UIBootgrid({
search:'/api/openvpn/instances/search_static_key/',
get:'/api/openvpn/instances/get_static_key/',
add:'/api/openvpn/instances/add_static_key/',
set:'/api/openvpn/instances/set_static_key/',
del:'/api/openvpn/instances/del_static_key/'
});
$("#instance\\.role").change(function(){
let show_advanced = $("#show_advanced_formDialogDialogInstance").hasClass("fa-toggle-on");
let this_role = $(this).val();
$(".role").each(function(){
let tr = $(this).closest("tr").hide();
if ((tr.data('advanced') === true && show_advanced) || !tr.data('advanced')) {
if ($(this).hasClass('role_' + this_role)) {
tr.show();
}
}
});
});
$("#show_advanced_formDialogDialogInstance").click(function(){
$("#instance\\.role").change();
});
// move "generate key" inside form dialog
$("#row_statickey\\.mode > td:eq(1) > div:last").before($("#keygen_div").detach().show());
$("#keygen").click(function(){
ajaxGet("/api/openvpn/instances/gen_key", {}, function(data, status){
if (data.result && data.result === 'ok') {
$("#statickey\\.key").val(data.key);
}
});
})
$("#reconfigureAct").SimpleActionButton();
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#instances">{{ lang._('Instances') }}</a></li>
<li><a data-toggle="tab" href="#statickeys">{{ lang._('Static Keys') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="instances" class="tab-pane fade in active">
<table id="grid-instances" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogInstance" data-editAlert="InstanceChangeMessage">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="role" data-type="string">{{ lang._('Role') }}</th>
<th data-column-id="dev_type" data-type="string">{{ lang._('Type') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<div id="InstanceChangeMessage" class="alert alert-info" style="display: none" role="alert">
{{ lang._('After changing settings, please remember to apply them') }}
</div>
<hr/>
<button class="btn btn-primary" id="reconfigureAct"
data-endpoint='/api/openvpn/service/reconfigure'
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring openvpn') }}"
type="button"
></button>
<br/><br/>
</div>
</div>
<div id="statickeys" class="tab-pane fade in">
<span id="keygen_div" style="display:none" class="pull-right">
<button id="keygen" type="button" class="btn btn-secondary" title="{{ lang._('Generate new.') }}" data-toggle="tooltip">
<i class="fa fa-fw fa-gear"></i>
</button>
</span>
<table id="grid-statickeys" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="DialogStaticKey">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogInstance,'id':'DialogInstance','label':lang._('Edit Instance')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogStaticKey,'id':'DialogStaticKey','label':lang._('Edit Static Key')])}}

View File

@ -27,7 +27,7 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("config.inc");
require_once("legacy_bindings.inc");
require_once("util.inc");
require_once("plugins.inc.d/openvpn.inc");
@ -36,25 +36,19 @@ openlog("openvpn", LOG_ODELAY, LOG_AUTH);
$common_name = getenv("common_name");
$vpnid = getenv("auth_server");
$config_file = getenv("config_file");
if (isset($config['openvpn']['openvpn-server'])) {
foreach ($config['openvpn']['openvpn-server'] as $server) {
if ($server['vpnid'] == $vpnid) {
// XXX: Eventually we should move the responsibility to determine if we do want to write a file
// to here instead of the configuration file (always call event, filter relevant).
$cso = (new OPNsense\OpenVPN\OpenVPN())->getOverwrite($vpnid, $common_name);
if (empty($cso)) {
$cso = array("common_name" => $common_name);
}
if (!empty($config_file)) {
$cso_filename = openvpn_csc_conf_write($cso, $server, $config_file);
if (!empty($cso_filename)) {
syslog(LOG_NOTICE, "client config created @ {$cso_filename}");
}
} else {
syslog(LOG_NOTICE, "unable to write client config for {$common_name}, missing target filename");
}
break;
$server = (new OPNsense\OpenVPN\OpenVPN())->getInstanceById($vpnid, 'server');
if ($server) {
$cso = (new OPNsense\OpenVPN\OpenVPN())->getOverwrite($vpnid, $common_name);
if (empty($cso)) {
$cso = array("common_name" => $common_name);
}
if (!empty($config_file)) {
$cso_filename = openvpn_csc_conf_write($cso, $server, $config_file);
if (!empty($cso_filename)) {
syslog(LOG_NOTICE, "client config created @ {$cso_filename}");
}
} else {
syslog(LOG_NOTICE, "unable to write client config for {$common_name}, missing target filename");
}
}

View File

@ -59,13 +59,15 @@ def ovpn_cmd(filename, cmd):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('server_id', help='server/client id (where to find socket)', type=int)
parser.add_argument('server_id', help='server/client id (where to find socket)', type=str)
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]:
if basename in [
'client%s.sock'%args.server_id, 'server%s.sock'%args.server_id, 'instance-%s.sock'%args.server_id
]:
socket_name = filename
break
if socket_name:

View File

@ -0,0 +1,183 @@
#!/usr/local/bin/php
<?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.
*/
require_once('script/load_phalcon.php');
require_once('util.inc');
require_once('interfaces.inc');
function setup_interface($instance)
{
if (!file_exists("/dev/{$instance->__devnode}")) {
mwexecf('/sbin/ifconfig %s create', [$instance->__devnode]);
}
if (!does_interface_exist($instance->__devname)) {
mwexecf('/sbin/ifconfig %s name %s', [$instance->__devnode, $instance->__devname]);
mwexecf('/sbin/ifconfig %s group openvpn', [$instance->__devname]);
}
}
function ovpn_start($instance, $fhandle)
{
setup_interface($instance);
if (!isvalidpid($instance->pidFilename)) {
if ($instance->role == 'server') {
if (is_file($instance->csoDirectory)) {
unlink($instance->csoDirectory);
}
@mkdir($instance->csoDirectory, 0750, true);
}
if (!mwexecf('/usr/local/sbin/openvpn --config %s', $instance->cnfFilename)) {
$pid = waitforpid($instance->pidFilename, 10);
if ($pid) {
syslog(LOG_NOTICE, "OpenVPN {$instance->role} {$instance->vpnid} instance started on PID {$pid}.");
} else {
syslog(LOG_WARNING, "OpenVPN {$instance->role} {$instance->vpnid} instance start timed out.");
}
}
// write instance details
$data = [
'md5' => md5_file($instance->cnfFilename),
'vpnid' => (string)$instance->vpnid,
'devname' => (string)$instance->__devname,
];
fseek($fhandle, 0);
ftruncate($fhandle, 0);
fwrite($fhandle, json_encode($data));
}
}
function ovpn_stop($instance)
{
killbypid($instance->pidFilename);
@unlink($instance->pidFilename);
@unlink($instance->sockFilename);
}
function ovpn_instance_stats($instance, $fhandle)
{
fseek($fhandle, 0);
$data = json_decode(stream_get_contents($fhandle) ?? '', true) ?? [];
$data['has_changed'] = ($data['md5'] ?? '') != @md5_file($instance->cnfFilename);
foreach (['vpnid', 'devname'] as $fieldname) {
$data[$fieldname] = $data[$fieldname] ?? null;
}
return $data;
}
$opts = getopt('ah', [], $optind);
$args = array_slice($argv, $optind);
/* setup syslog logging */
openlog("openvpn", LOG_ODELAY, LOG_AUTH);
if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', 'restart', 'configure'])) {
echo "Usage: ovpn_service_control.php [-a] [-h] [stop|start|restart|configure] [uuid]\n\n";
echo "\t-a all instances\n";
} elseif (isset($opts['a']) || !empty($args[1])) {
$mdl = new OPNsense\OpenVPN\OpenVPN();
$instance_id = $args[1] ?? null;
$action = $args[0];
if ($action != 'stop') {
$mdl->generateInstanceConfig($instance_id);
}
$instance_ids = [];
foreach ($mdl->Instances->Instance->iterateItems() as $key => $node) {
if (empty((string)$node->enabled)) {
continue;
}
if ($instance_id != null && $key != $instance_id) {
continue;
}
$instance_ids[] = $key;
$statHandle = fopen($node->statFilename, "a+");
if (flock($statHandle, LOCK_EX)) {
$instance_stats = ovpn_instance_stats($node, $statHandle);
switch ($action) {
case 'stop':
ovpn_stop($node);
break;
case 'start':
ovpn_start($node, $statHandle);
break;
case 'restart':
ovpn_stop($node);
ovpn_start($node, $statHandle);
break;
case 'configure':
if ($instance_stats['has_changed']) {
ovpn_stop($node);
ovpn_start($node, $statHandle);
}
break;
}
// cleanup old interface when needed
if (!empty($instance_stats['devname']) && $instance_stats['devname'] != $node->__devname) {
legacy_interface_destroy($instance_stats['devname']);
}
flock($statHandle, LOCK_UN);
}
fclose($statHandle);
}
/**
* When -a is specified, cleaup up old or disabled instances
*/
if ($instance_id == null) {
$to_clean = [];
foreach (glob('/var/etc/openvpn/instance-*') as $filename) {
$uuid = explode('.', explode('/var/etc/openvpn/instance-', $filename)[1])[0];
if (!in_array($uuid, $instance_ids)) {
if (!isset($to_clean[$uuid])) {
$to_clean[$uuid] = ['filenames' => [], 'stat' => []];
}
$to_clean[$uuid]['filenames'][] = $filename;
if (str_ends_with($filename, '.stat')) {
$to_clean[$uuid]['stat'] = json_decode(file_get_contents($filename) ?? '', true) ?? [];
}
}
}
foreach ($to_clean as $uuid => $payload) {
$pidfile = "/var/run/ovpn-instance-{$uuid}.pid";
if (isvalidpid($pidfile)) {
killbypid($pidfile);
}
@unlink($pidfile);
if (is_array($payload['stat']) && !empty($payload['stat']['devname'])) {
legacy_interface_destroy($payload['stat']['devname']);
}
foreach ($payload['filenames'] as $filename) {
@unlink($filename);
}
}
}
closelog();
}

View File

@ -113,8 +113,22 @@ def main(params):
response = {}
for filename in glob.glob('/var/etc/openvpn/*.sock'):
bname = os.path.basename(filename)[:-5]
this_id = bname[6:]
if bname.startswith('server') and 'server' in params.options:
this_id = bname[9:] if bname.startswith('inst') else bname[6:]
if bname.startswith('instance-'):
this_status = ovpn_status(filename)
role = 'client' if 'bytes_received' in this_status else 'server'
if role not in response:
response[role] = {}
if role == 'server' and 'server' in params.options:
response['server'][this_id] = this_status
if 'status' not in response['server'][this_id]:
# p2p mode, no client_list or routing_table
response['server'][this_id].update(ovpn_state(filename))
elif role == 'client' and 'client' in params.options:
response['client'][this_id] = ovpn_state(filename)
if response['client'][this_id]['status'] != 'failed':
response['client'][this_id].update(this_status)
elif bname.startswith('server') and 'server' in params.options:
if 'server' not in response:
response['server'] = {}
response['server'][this_id] = ovpn_status(filename)

View File

@ -33,22 +33,6 @@ require_once("util.inc");
require_once("interfaces.inc");
require_once("plugins.inc.d/openvpn.inc");
/**
* @param string $serverid server identifier
* @return array|null openvpn server properties
*/
function get_openvpn_server($serverid)
{
global $config;
if (isset($config['openvpn']['openvpn-server'])) {
foreach ($config['openvpn']['openvpn-server'] as $server) {
if ($server['vpnid'] == $serverid) {
return $server;
}
}
}
return null;
}
/**
* Parse provisioning properties supplied by the authenticator
@ -108,7 +92,7 @@ function do_auth($common_name, $serverid, $method, $auth_file)
}
}
}
$a_server = $serverid !== null ? get_openvpn_server($serverid) : null;
$a_server = $serverid !== null ? (new OPNsense\OpenVPN\OpenVPN())->getInstanceById($serverid, 'server') : null;
if ($a_server == null) {
return "OpenVPN '$serverid' was not found. Denying authentication for user {$username}";
} elseif (!empty($a_server['strictusercn']) && $username != $common_name) {

View File

@ -9,3 +9,33 @@ command:/usr/local/opnsense/scripts/openvpn/kill_session.py
parameters: %s %s
type:script_output
message:Kill OpenVPN session %s - %s
[genkey]
command:/usr/local/sbin/openvpn --genkey secret /dev/stdout
parameters:
type:script_output
message: Generate new OpenVPN static key
[start]
command:/usr/local/opnsense/scripts/openvpn/ovpn_service_control.php
parameters: start %s
type:script
message: start openvpn instance %s
[stop]
command:/usr/local/opnsense/scripts/openvpn/ovpn_service_control.php
parameters: stop %s
type:script
message: stop openvpn instance %s
[restart]
command:/usr/local/opnsense/scripts/openvpn/ovpn_service_control.php
parameters: restart %s
type:script
message: restart openvpn instance %s
[configure]
command:/usr/local/opnsense/scripts/openvpn/ovpn_service_control.php
parameters: -a configure
type:script
message: configure openvpn instances

View File

@ -29,6 +29,52 @@
require_once("guiconfig.inc");
require_once("plugins.inc.d/openvpn.inc");
function openvpn_config()
{
global $config;
$result = [];
foreach (['openvpn-server', 'openvpn-client'] as $section) {
$result[$section] = [];
if (!empty($config['openvpn'][$section])) {
foreach ($config['openvpn'][$section] as $settings) {
if (empty($settings) || isset($settings['disable'])) {
continue;
}
$server = [];
$default_port = ($section == 'openvpn-server') ? 1194 : '';
$server['port'] = !empty($settings['local_port']) ? $settings['local_port'] : $default_port;
$server['mode'] = $settings['mode'];
if (empty($settings['description'])) {
$settings['description'] = ($section == 'openvpn-server') ? 'Server' : 'Client';
}
$server['name'] = "{$settings['description']} {$settings['protocol']}:{$server['port']}";
$server['vpnid'] = $settings['vpnid'];
$result[$section][] = $server;
}
}
}
foreach ((new OPNsense\OpenVPN\OpenVPN())->Instances->Instance->iterateItems() as $key => $node) {
if (!empty((string)$node->enabled)) {
$section = "openvpn-{$node->role}";
$default_port = ($section == 'openvpn-server') ? 1194 : '';
$default_desc = ($section == 'openvpn-server') ? 'Server' : 'Client';
$server = [
'port' => !empty((string)$node->port) ? (string)$node->port : $default_port,
'mode' => (string)$node->role,
'description' => !empty((string)$node->description) ? (string)$node->description : $default_desc,
'name' => "{$node->description} {$node->proto}:{$node->port}",
'vpnid' => $key
];
$result[$section][] = $server;
}
}
return $result;
}
$openvpn_status = json_decode(configd_run('openvpn connections client,server'), true) ?? [];
$openvpn_cfg = openvpn_config();
foreach ($openvpn_cfg as $section => &$ovpncfg) {