diff --git a/src/etc/inc/plugins.inc.d/openvpn.inc b/src/etc/inc/plugins.inc.d/openvpn.inc index 4092dc136..801950274 100644 --- a/src/etc/inc/plugins.inc.d/openvpn.inc +++ b/src/etc/inc/plugins.inc.d/openvpn.inc @@ -1,8 +1,8 @@ - * Copyright (C) 2016 Deciso B.V. * Copyright (C) 2008 Scott Ullrich * Copyright (C) 2006 Fernando Lemos * Copyright (C) 2005 Peter Allgeyer @@ -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); + } + } } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ExportController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ExportController.php index 769684837..9feef9dc6 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ExportController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ExportController.php @@ -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'] ?? ''; diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php new file mode 100644 index 000000000..59de1f5e2 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php @@ -0,0 +1,107 @@ +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']; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php index f5a2dbc8f..530657776 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/ServiceController.php @@ -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']; } diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/InstancesController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/InstancesController.php new file mode 100644 index 000000000..7690ae4f6 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/InstancesController.php @@ -0,0 +1,49 @@ +view->pick('OPNsense/OpenVPN/instances'); + $this->view->formDialogInstance = $this->getForm('dialogInstance'); + $this->view->formDialogStaticKey = $this->getForm('dialogStaticKey'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogInstance.xml b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogInstance.xml new file mode 100644 index 000000000..e5d37b455 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogInstance.xml @@ -0,0 +1,333 @@ + + + header + + + + instance.vpnid + + + + text + + + instance.role + + dropdown + + Define the role of this instance + + + + instance.description + + text + You may enter a description here for your reference (not parsed). + + + instance.enabled + + checkbox + + + instance.proto + + dropdown + Use protocol for communicating with remote host. + + + instance.port + + text + Portnumber to use, defaults to 1194 when in server role, nobind for clients + + + instance.local + + text + + Optional IP address for bind. If specified, OpenVPN will bind to this address only. + If unspecified, OpenVPN will bind to all interfaces. + + + + instance.dev_type + + dropdown + true + + 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. + + + + instance.verb + + dropdown + true + Output verbosity level (0..9) + + + instance.maxclients + + true + + text + Specify the maximum number of clients allowed to concurrently connect to this server. + + + instance.keepalive_interval + + true + text + Ping interval in seconds. 0 to disable keep alive + + + instance.keepalive_timeout + + true + text + Causes OpenVPN to restart after n seconds pass without reception of a ping or other packet from remote. + + + instance.server + + text + + + 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 + + + + instance.server_ipv6 + + text + + + 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 + + + + instance.topology + + dropdown + + + 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. + + + + instance.remote + + select_multiple + true + + 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 + + + + header + + + + instance.cert + + dropdown + Select a certificate to use for this service. + + + instance.crl + + dropdown + Select a certificate revocation list to use for this service. + + + instance.verify_client_cert + + dropdown + Specify if the client is required to offer a certificate. + + + instance.tls_key + + dropdown + + 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). + + + + instance.data-ciphers + + select_multiple + true + + Restrict the allowed ciphers to be negotiated to the ciphers in this list. + + + instance.data-ciphers-fallback + + dropdown + true + + + 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). + + + + header + + + + instance.authmode + + select_multiple + + Select authentication methods to use, leave empty if no challenge response authentication is needed. + + + instance.local_group + + dropdown + + Restrict access to users in the selected local group. Please be aware that other authentication backends will refuse to authenticate when using this option. + + + instance.username_as_common_name + + checkbox + true + + Use the authenticated username as the common-name, rather than the common-name from the client certificate. + + + instance.strictusercn + + checkbox + + When authenticating users, enforce a match between the Common Name of the client certificate and the username given at login. + + + instance.username + + text + + (optional) Username to send to the server for authentication when required. + + + instance.password + + password + + Password belonging to the user specified above + + + instance.reneg-sec + + text + 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. + + + + instance.auth-gen-token + + text + + 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. + + + + header + + + + instance.push_route + + select_multiple + + true + These are the networks accessible on this host, these are pushed via route{-ipv6} clauses in OpenVPN to the client. + + + instance.route + + select_multiple + + true + Remote networks for the server, add route to routing table after connection is established + + + header + + + + instance.various_flags + + select_multiple + Various less frequently used yes/no options which can be set for this instance. + + + instance.redirect_gateway + + select_multiple + + Automatically execute routing commands to cause all outgoing IP traffic to be redirected over the VPN. + + + instance.register_dns + + checkbox + + Run ipconfig /flushdns and ipconfig /registerdns on connection initiation. This is known to kick Windows into recognizing pushed DNS servers. + + + instance.dns_domain + + text + + Set Connection-specific DNS Suffix. + + + instance.dns_domain_search + + select_multiple + + true + + Add name to the domain search list. Repeat this option to add more entries. Up to 10 domains are supported + + + + instance.dns_servers + + select_multiple + + true + + Set primary domain name server IPv4 or IPv6 address. Repeat this option to set secondary DNS server addresses. + + + + instance.ntp_servers + + select_multiple + + true + + Set primary NTP server address (Network Time Protocol). Repeat this option to set secondary NTP server addresses. + + + diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogStaticKey.xml b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogStaticKey.xml new file mode 100644 index 000000000..f1a9d2a84 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/forms/dialogStaticKey.xml @@ -0,0 +1,21 @@ + + + statickey.description + + text + You may enter a description here for your reference (not parsed). + + + statickey.mode + + 220px + dropdown + Define the use of this key, authentication (--tls-auth) or authentication and encryption (--tls-crypt) + + + statickey.key + + textbox + Paste an OpenVPN Static key. Or generate one with the button. + + \ No newline at end of file diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index 70e81bc5d..5edd42167 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -747,6 +747,13 @@ api/openvpn/client_overwrites/* + + VPN: OpenVPN: Instances + + ui/openvpn/instances + api/openvpn/instances/* + + VPN: OpenVPN: Server diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index 285de0de0..c64becc9b 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -204,6 +204,7 @@ + diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Export.xml b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Export.xml index 4c25ec080..10050ad39 100644 --- a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Export.xml +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/Export.xml @@ -5,7 +5,7 @@ - + Y diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/InstanceField.php b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/InstanceField.php new file mode 100644 index 000000000..e6cfbd0aa --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/InstanceField.php @@ -0,0 +1,63 @@ +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(); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/RemoteHostField.php b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/RemoteHostField.php new file mode 100644 index 000000000..ffaadc726 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/RemoteHostField.php @@ -0,0 +1,95 @@ +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; + } +} \ No newline at end of file diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/VPNIdField.php b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/VPNIdField.php new file mode 100644 index 000000000..abb36d663 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/FieldTypes/VPNIdField.php @@ -0,0 +1,97 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.php b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.php index d3899c7fe..0fc07fa27 100644 --- a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.php +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.php @@ -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 .= "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["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[''] = $tmp['prv']; + $options[''] = $tmp['crt']; + if (isset($tmp['ca'])) { + $options[''] = $tmp['ca']['crt']; + } + } + } + // dump to file + $this->writeConfig($node->cnfFilename, $options); + } + } + } } diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml index d59e959a6..a11dcec95 100644 --- a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml @@ -93,5 +93,265 @@ + + + + Y + + + 1 + Y + + + Y + tun + + tun + tap + + + + Y + 3 + + 0 (No output except fatal errors.) + 1 (Normal) + 2 (Normal) + 3 (Normal) + 4 (Normal) + 5 (log packets) + 6 (debug) + 7 (debug) + 8 (debug) + 9 (debug) + 10 (debug) + 11 (debug) + + + + Y + udp + + UDP + UDP (IPv4) + UDP (IPv6) + TCP + TCP (IPv4) + TCP (IPv6) + + + + + + N + N + N + + + Y + subnet + + net30 + p2p + subnet + + + + + + server + Y + + Client + Server + + + + N + N + + + N + N + + + , + N + Y + N + + + , + N + Y + N + + + N + None + Please select a valid certificate from the list + + + N + crl + None + Please select a valid certificate from the list + + + Y + require + + none + required + + + + N + Y + + AES-256-GCM + AES-128-GCM + CHACHA20-POLY1305 + + + + N + + AES-256-GCM + AES-128-GCM + CHACHA20-POLY1305 + + + + + + OPNsense.OpenVPN.OpenVPN + StaticKeys.StaticKey + mode,description + [%s] %s + + + N + + + N + Y + None + Local Database + + + N + + + N + Y + + client-to-client + duplicate-cn + passtos + persist-remote-ip + route-nopull + route-noexec + remote-random + + + + 0 + Y + + + 0 + Y + + + + + + + 1 + 65535 + + + 10 + 0 + 65535 + + + 60 + 0 + 65535 + + + 0 + 65535 + N + + + 0 + 65535 + N + + + N + Y + + local + autolocal + default + bypass dhcp + bypass dns + block local + ipv6 (default) + not ipv4 (default) + + + + 0 + Y + + + N + + + N + , + Y + + + N + N + , + Y + + + N + N + , + Y + + + N + + + + + + + Y + crypt + + auth (Authenticate control channel packets) + crypt (Encrypt and authenticate all control channel packets) + + + + Y + A key is required, generate one with the button + + + N + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt new file mode 100644 index 000000000..e7a6cb9fe --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt @@ -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. + #} + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Role') }}{{ lang._('Type') }}{{ lang._('Commands') }}
+ + +
+
+ +
+ +

+
+
+
+ + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+ +{{ 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')])}} + diff --git a/src/opnsense/scripts/openvpn/client_connect.php b/src/opnsense/scripts/openvpn/client_connect.php index 4e63dfa3b..83b55216a 100755 --- a/src/opnsense/scripts/openvpn/client_connect.php +++ b/src/opnsense/scripts/openvpn/client_connect.php @@ -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"); } } diff --git a/src/opnsense/scripts/openvpn/kill_session.py b/src/opnsense/scripts/openvpn/kill_session.py index 0bf0a103a..c2257181f 100755 --- a/src/opnsense/scripts/openvpn/kill_session.py +++ b/src/opnsense/scripts/openvpn/kill_session.py @@ -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: diff --git a/src/opnsense/scripts/openvpn/ovpn_service_control.php b/src/opnsense/scripts/openvpn/ovpn_service_control.php new file mode 100755 index 000000000..489d9449a --- /dev/null +++ b/src/opnsense/scripts/openvpn/ovpn_service_control.php @@ -0,0 +1,183 @@ +#!/usr/local/bin/php +__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(); +} + diff --git a/src/opnsense/scripts/openvpn/ovpn_status.py b/src/opnsense/scripts/openvpn/ovpn_status.py index cdd7d17df..99dc574ec 100755 --- a/src/opnsense/scripts/openvpn/ovpn_status.py +++ b/src/opnsense/scripts/openvpn/ovpn_status.py @@ -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) diff --git a/src/opnsense/scripts/openvpn/user_pass_verify.php b/src/opnsense/scripts/openvpn/user_pass_verify.php index 69702786b..518d637eb 100755 --- a/src/opnsense/scripts/openvpn/user_pass_verify.php +++ b/src/opnsense/scripts/openvpn/user_pass_verify.php @@ -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) { diff --git a/src/opnsense/service/conf/actions.d/actions_openvpn.conf b/src/opnsense/service/conf/actions.d/actions_openvpn.conf index 134037b72..6c0da4b08 100644 --- a/src/opnsense/service/conf/actions.d/actions_openvpn.conf +++ b/src/opnsense/service/conf/actions.d/actions_openvpn.conf @@ -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 diff --git a/src/www/widgets/widgets/openvpn.widget.php b/src/www/widgets/widgets/openvpn.widget.php index ff47238fb..9d810c57b 100644 --- a/src/www/widgets/widgets/openvpn.widget.php +++ b/src/www/widgets/widgets/openvpn.widget.php @@ -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) {