diff --git a/src/etc/inc/openvpn.inc b/src/etc/inc/openvpn.inc index 1983ad5f67c16b62560e1ddd2328cc1e978c47eb..c117fc245379bc4e808bda1d091ec96288fc298c 100644 --- a/src/etc/inc/openvpn.inc +++ b/src/etc/inc/openvpn.inc @@ -821,6 +821,24 @@ function openvpn_add_keyfile(& $data, & $conf, $mode_id, $directive, $opt = "") $conf .= "{$directive} {$fpath} {$opt}\n"; } +function openvpn_delete_tmp($mode, $id) { + global $g; + + /* delete temporary files created by connect script */ + if (($mode == "server") && (isset($id))) { + unlink_if_exists("{$g['tmp_path']}/ovpn_ovpns{$id}_*.rules"); + } + /* delete temporary files created by OpenVPN */ + $tmpfiles = array_filter(glob("{$g['tmp_path']}/openvpn_cc*.tmp"),'is_file'); + if (!empty($tmpfiles)) { + foreach ($tmpfiles as $tmpfile) { + if ((time() - filemtime($tmpfile)) > 60) { + @unlink_if_exists($tmpfile); + } + } + } +} + function openvpn_reconfigure($mode, $settings) { global $g, $config, $openvpn_tls_server_modes, $openvpn_dh_lengths, $openvpn_default_keepalive_interval, $openvpn_default_keepalive_timeout; @@ -1175,9 +1193,13 @@ function openvpn_reconfigure($mode, $settings) { } break; } + $connlimit = "0"; if ($settings['mode'] != 'p2p_shared_key' && isset($settings['duplicate_cn'])) { $conf .= "duplicate-cn\n"; + if ($settings['connlimit']) { + $connlimit = "{$settings['connlimit']}"; + } } if (($settings['mode'] != 'p2p_shared_key') && isset($settings['remote_cert_tls'])) { @@ -1518,6 +1540,8 @@ function openvpn_reconfigure($mode, $settings) { unset($conf); $fpath = "{$g['openvpn_base']}/{$mode_id}/interface"; file_put_contents($fpath, $interface); + $fpath = "{$g['openvpn_base']}/{$mode_id}/connuserlimit"; + file_put_contents($fpath, $connlimit); //chown($fpath, 'nobody'); //chgrp($fpath, 'nobody'); @chmod("{$g['openvpn_base']}/{$mode_id}/config.ovpn", 0600); @@ -1525,6 +1549,7 @@ function openvpn_reconfigure($mode, $settings) { @chmod("{$g['openvpn_base']}/{$mode_id}/key", 0600); @chmod("{$g['openvpn_base']}/{$mode_id}/tls-auth", 0600); @chmod("{$g['openvpn_base']}/{$mode_id}/conf", 0600); + @chmod("{$g['openvpn_base']}/{$mode_id}/connuserlimit", 0600); if ($wait_tentative) { interface_wait_tentative($interface); @@ -1537,7 +1562,6 @@ function openvpn_restart($mode, $settings) { $vpnid = $settings['vpnid']; $mode_id = $mode.$vpnid; $lockhandle = lock("openvpnservice{$mode_id}", LOCK_EX); - openvpn_clean_rules($mode, $vpnid); openvpn_reconfigure($mode, $settings); /* kill the process if running */ $pfile = $g['varrun_path']."/openvpn_{$mode_id}.pid"; @@ -1570,6 +1594,8 @@ function openvpn_restart($mode, $settings) { return; } + openvpn_delete_tmp($mode, $vpnid); + /* Do not start an instance if we are not CARP master on this vip! */ if (strstr($settings['interface'], "_vip")) { $carpstatus = get_carp_bind_status($settings['interface']); @@ -1649,7 +1675,7 @@ function openvpn_delete($mode, $settings) { /* remove the configuration files */ unlink_if_exists("{$g['openvpn_base']}/{$mode_id}/*/*"); unlink_if_exists("{$g['openvpn_base']}/{$mode_id}/*"); - openvpn_clean_rules($mode, $vpnid); + openvpn_delete_tmp($mode, $vpnid); } function openvpn_resync_csc(& $settings) { @@ -2405,12 +2431,4 @@ function openvpn_inuse($id, $mode) { return false; } -function openvpn_clean_rules($mode, $id) { - global $g; - - if ($mode == "server") { - unlink_if_exists("{$g['tmp_path']}/ovpn_ovpns{$id}_*.rules"); - } -} - ?> diff --git a/src/etc/inc/service-utils.inc b/src/etc/inc/service-utils.inc index 35a74f74af9710e8e927e4136956f368c3dcd1d3..3228ecc14444f5a5cec744ab962e6adbbbe143fd 100644 --- a/src/etc/inc/service-utils.inc +++ b/src/etc/inc/service-utils.inc @@ -763,7 +763,7 @@ function service_control_stop($name, $extras) { $id = htmlspecialchars($extras['id']); $pidfile = "{$g['varrun_path']}/openvpn_{$vpnmode}{$id}.pid"; killbypid($pidfile); - openvpn_clean_rules($vpnmode, $id); + openvpn_delete_tmp($vpnmode, $id); } break; case 'syslogd': diff --git a/src/usr/local/sbin/openvpn.attributes.sh b/src/usr/local/sbin/openvpn.attributes.sh index 51ce83b95ec15bbbd3f8edc98b7bc005869ebb99..b66614aaca31bda9c3272653a6558c8c3c69810e 100755 --- a/src/usr/local/sbin/openvpn.attributes.sh +++ b/src/usr/local/sbin/openvpn.attributes.sh @@ -20,76 +20,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [ -z "${untrusted_ip6}" ]; then - ipaddress="${untrusted_ip}" -else - ipaddress="${untrusted_ip6}" -fi - -# Remote Access (SSL/TLS) mode -if [ -z "${username}" ]; then - if [ "$script_type" = "client-connect" ]; then - /usr/bin/logger -t openvpn "openvpn server '${dev}' user cert CN '${X509_0_CN}' address '${ipaddress}' - connected" - elif [ "$script_type" = "client-disconnect" ]; then - /usr/bin/logger -t openvpn "openvpn server '${dev}' user cert CN '${X509_0_CN}' address '${ipaddress}' - disconnected" - /sbin/pfctl -k $ifconfig_pool_remote_ip - /sbin/pfctl -K $ifconfig_pool_remote_ip - /sbin/pfctl -k $ifconfig_pool_remote_ip6 - /sbin/pfctl -K $ifconfig_pool_remote_ip6 +# Signal deferred handler +if [ "${script_type}" = "client-connect" ]; then + /bin/echo 2 > "${client_connect_deferred_file}" + if [ -f /tmp/"${common_name}" ]; then + /bin/cat /tmp/"${common_name}" > "${client_connect_config_file}" + /bin/rm /tmp/"${common_name}" fi - exit 0 fi -lockfile="/tmp/ovpn_${dev}_${username}_${trusted_port}.lock" -rulesfile="/tmp/ovpn_${dev}_${username}_${trusted_port}.rules" -anchorname="openvpn/${dev}_${username}_${trusted_port}" - -if [ "$script_type" = "client-connect" ]; then - /usr/bin/logger -t openvpn "openvpn server '${dev}' user '${username}' address '${ipaddress}' - connected" - i=1 - while [ -f "${lockfile}" ]; do - if [ $i -ge 30 ]; then - /bin/echo "Timeout while waiting for lockfile" - exit 1 - fi - - /bin/sleep 1 - i=$(( i + 1 )) - done - /usr/bin/touch "${lockfile}" - - /bin/cat "${rulesfile}" | /usr/bin/sed "s/{clientip}/${ifconfig_pool_remote_ip}/g" | /usr/bin/sed "s/{clientipv6}/${ifconfig_pool_remote_ip6}/g" > "${rulesfile}.tmp" && /bin/mv "${rulesfile}.tmp" "${rulesfile}" - /sbin/pfctl -a "openvpn/${dev}_${username}_${trusted_port}" -f "${rulesfile}" - - if [ -f /tmp/$common_name ]; then - /bin/cat /tmp/$common_name > $1 - /bin/rm /tmp/$common_name - fi - - /bin/rm "${lockfile}" -elif [ "$script_type" = "client-disconnect" ]; then - /usr/bin/logger -t openvpn "openvpn server '${dev}' user '${username}' address '${ipaddress}' - disconnected" - i=1 - while [ -f "${lockfile}" ]; do - if [ $i -ge 30 ]; then - /bin/echo "Timeout while waiting for lockfile" - exit 1 - fi - - /bin/sleep 1 - i=$(( i + 1 )) - done - /usr/bin/touch "${lockfile}" - - command="/sbin/pfctl -a '${anchorname}' -F rules" - eval $command - /sbin/pfctl -k $ifconfig_pool_remote_ip - /sbin/pfctl -K $ifconfig_pool_remote_ip - /sbin/pfctl -k $ifconfig_pool_remote_ip6 - /sbin/pfctl -K $ifconfig_pool_remote_ip6 - - /bin/rm "${rulesfile}" - /bin/rm "${lockfile}" -fi +# Handle 'client-connect' and 'client-disconnect' +/usr/bin/nohup /usr/local/sbin/openvpn.connect_async.sh > /dev/null & +# Signal "deferred handler started OK" for client-connect exit 0 diff --git a/src/usr/local/sbin/openvpn.connect_async.sh b/src/usr/local/sbin/openvpn.connect_async.sh new file mode 100755 index 0000000000000000000000000000000000000000..64ba4cf97ea9140e0070308e57f86785fb7c18fb --- /dev/null +++ b/src/usr/local/sbin/openvpn.connect_async.sh @@ -0,0 +1,174 @@ +#!/bin/sh +# +# openvpn.connect_async.sh +# +# part of pfSense (https://www.pfsense.org) +# Copyright (c) 2021-2022 Rubicon Communications, LLC (Netgate) +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +log_session() { + if [ -z "${1}" ]; then + logmsg="" + else + logmsg=" - ${1}" + fi + + if [ -z "${untrusted_ip6}" ]; then + hostaddress="${untrusted_ip}:${untrusted_port}" + else + hostaddress="${untrusted_ip6}:${untrusted_port}" + fi + + if [ -z "${username}" ]; then + hostuser="user cert CN '${X509_0_CN}'" + else + hostuser="user '${username}'" + fi + + /usr/bin/logger -t openvpn "openvpn server '${dev}' ${hostuser} address '${hostaddress}'${logmsg}" +} + +if [ -n "${username}" ]; then + lockfile="/tmp/ovpn_${dev}_${username}_${trusted_port}.lock" + rulesfile="/tmp/ovpn_${dev}_${username}_${trusted_port}.rules" + anchorname="openvpn/${dev}_${username}_${trusted_port}" +fi + +if [ "${script_type}" = "client-disconnect" ]; then + log_session "disconnected" + + if [ -n "${username}" ]; then + # Avoid race condition. See https://redmine.pfsense.org/issues/9206 + i=1 + while + if [ -f "${lockfile}" ]; then + /bin/sleep 1 + i="$((i+1))" + else + break + fi + [ "${i}" -lt 30 ] + do :; done + + if [ ${i} -ge 30 ]; then + log_session "Timeout while waiting for lockfile" + else + /usr/bin/touch "${lockfile}" + eval "/sbin/pfctl -a '${anchorname}' -F rules" + /bin/rm "${lockfile}" + + /bin/rm "${rulesfile}" + fi + fi + + /sbin/pfctl -k $ifconfig_pool_remote_ip + /sbin/pfctl -K $ifconfig_pool_remote_ip + /sbin/pfctl -k $ifconfig_pool_remote_ip6 + /sbin/pfctl -K $ifconfig_pool_remote_ip6 +elif [ "${script_type}" = "client-connect" ]; then + log_session "connecting" + + # Verify defer status code before continuing + i=1 + while + deferstatus=$(/usr/bin/head -1 "${client_connect_deferred_file}") + if [ "${deferstatus}" -ne 2 ]; then + /bin/sleep 1 + i="$((i+1))" + else + break + fi + [ "${i}" -lt 3 ] + do :; done + if [ ${i} -ge 3 ]; then + log_session "server write to defer file failed" + /bin/echo 0 > ${client_connect_deferred_file} + exit 1 + fi + + # Get active sessions + # active_sessions :: ovpns1_'user_01'_30001|ovpns1_'user_01'_30002|ovpns1_'user_01'_30003| + # Use php-cgi - see https://redmine.pfsense.org/issues/12382 + active_sessions=$("/usr/local/bin/php-cgi" -f "/usr/local/sbin/openvpn_connect_async.php") + + # Process "Duplicate Connection Limit" setting + if [ -n "${active_sessions}" ]; then + vpnid=$(/bin/echo ${dev} | /usr/bin/sed -e 's/ovpns//g') + if [ -f "/var/etc/openvpn/server${vpnid}/connuserlimit" ]; then + sessionlimit=$(/usr/bin/head -1 "/var/etc/openvpn/server${vpnid}/connuserlimit" | /usr/bin/sed -e 's/[[:space:]]//g') + if [ "${sessionlimit}" -ge 1 ]; then + if [ -z "${username}" ]; then + usersession="${dev}_'${X509_0_CN}'" + else + usersession="${dev}_'${username}'" + fi + sessioncount=$(/bin/echo "${active_sessions}" | /usr/bin/grep -o "${usersession}" | /usr/bin/wc -l | /usr/bin/sed -e 's/[[:space:]]//g') + + if [ ${sessioncount} -gt ${sessionlimit} ]; then + log_session "active connection limit of '${sessionlimit}' reached" + /bin/echo 0 > ${client_connect_deferred_file} + if [ -n "${username}" ]; then + /bin/rm "${rulesfile}" + fi + exit 1 + fi + fi + fi + fi + + if [ -n "${username}" ]; then + + i=1 + while + if [ -f "${lockfile}" ]; then + /bin/sleep 1 + i="$((i+1))" + else + break + fi + [ "${i}" -lt 30 ] + do :; done + if [ ${i} -ge 30 ]; then + log_session "Timeout while waiting for lockfile" + /bin/echo 0 > ${client_connect_deferred_file} + exit 1 + else + /usr/bin/touch "${lockfile}" + + # for each of this user's anchors loaded in pf + # $session :: ovpns3_'user_01'_61468 + # $anchor :: openvpn/ovpns3_user_01_61468 + anchors=$(/sbin/pfctl -s Anchors) + for anchor in $(/bin/echo "${anchors}" | /usr/bin/grep "${dev}_${username}"); do + session=$(/bin/echo "${anchor}" | /usr/bin/sed -r -e 's/.+'"${dev}_${username}"'/'"${dev}_\'${username}\'"'/') + # if no active session exists for the anchor, remove it from pf + if ! (/bin/echo "${active_sessions}" | /usr/bin/grep -q "${session}"); then + eval "/sbin/pfctl -a '${anchor}' -F rules" + fi + done + + /bin/echo "$(/usr/bin/sed -e "s/{clientip}/${ifconfig_pool_remote_ip}/g;s/{clientipv6}/${ifconfig_pool_remote_ip6}/g" "${rulesfile}")" > "${rulesfile}" + eval "/sbin/pfctl -a '${anchorname}' -f '${rulesfile}'" + + /bin/rm "${lockfile}" + fi + fi + + # success; allow client connection + /bin/echo 1 > ${client_connect_deferred_file} + log_session "connected" +fi + +exit 0 diff --git a/src/usr/local/sbin/openvpn_connect_async.php b/src/usr/local/sbin/openvpn_connect_async.php new file mode 100644 index 0000000000000000000000000000000000000000..6aee3313735ed3529f37819db8091fece3e4f488 --- /dev/null +++ b/src/usr/local/sbin/openvpn_connect_async.php @@ -0,0 +1,51 @@ + diff --git a/src/usr/local/www/vpn_openvpn_server.php b/src/usr/local/www/vpn_openvpn_server.php index 379cc9a0c865538368fdb55264ef67a7ebc0f54f..139c46e7e988d64a9f1768840315d8d12a225cfd 100644 --- a/src/usr/local/www/vpn_openvpn_server.php +++ b/src/usr/local/www/vpn_openvpn_server.php @@ -189,6 +189,7 @@ if (($act == "edit") || ($act == "dup")) { $pconfig['local_network'] = $a_server[$id]['local_network']; $pconfig['local_networkv6'] = $a_server[$id]['local_networkv6']; $pconfig['maxclients'] = $a_server[$id]['maxclients']; + $pconfig['connlimit'] = $a_server[$id]['connlimit']; $pconfig['allow_compression'] = $a_server[$id]['allow_compression']; $pconfig['compression'] = $a_server[$id]['compression']; $pconfig['compression_push'] = $a_server[$id]['compression_push']; @@ -497,6 +498,10 @@ if ($_POST['save']) { $input_errors[] = gettext("The field 'Concurrent connections' must be numeric."); } + if ($pconfig['connlimit'] && !is_numericint($pconfig['connlimit'])) { + $input_errors[] = gettext("The field 'Duplicate Connection Limit' must be numeric."); + } + if (!array_key_exists($pconfig['topology'], $openvpn_topologies)) { $input_errors[] = gettext("The field 'Topology' contains an invalid selection"); } @@ -671,6 +676,7 @@ if ($_POST['save']) { $server['local_network'] = $pconfig['local_network']; $server['local_networkv6'] = $pconfig['local_networkv6']; $server['maxclients'] = $pconfig['maxclients']; + $server['connlimit'] = $pconfig['connlimit']; $server['allow_compression'] = $pconfig['allow_compression']; $server['compression'] = $pconfig['compression']; $server['compression_push'] = $pconfig['compression_push']; @@ -1338,6 +1344,13 @@ if ($act=="new" || $act=="edit"): 'When unset, a new connection from a user will disconnect the previous session. %1$s%1$s' . 'Users are identified by their username or certificate properties, depending on the VPN configuration. ' . 'This practice is discouraged security reasons, but may be necessary in some environments.', '
'); + + $section->addInput(new Form_Input( + 'connlimit', + 'Duplicate Connection Limit', + 'number', + $pconfig['connlimit'] + ))->setHelp('Limit the number of concurrent connections from the same user.'); $form->add($section); diff --git a/src/usr/local/www/wizards/openvpn_wizard.inc b/src/usr/local/www/wizards/openvpn_wizard.inc index 0d6eb6665bfd5849fae03678568dd49ffec158a3..a6a17810ba353124e5a979993a399992a5143703 100644 --- a/src/usr/local/www/wizards/openvpn_wizard.inc +++ b/src/usr/local/www/wizards/openvpn_wizard.inc @@ -452,6 +452,9 @@ function step10_submitphpaction() { if ($_POST['concurrentcon'] && !is_numeric($_POST['concurrentcon'])) $input_errors[] = "The field 'Concurrent connections' must be numeric."; + + if ($_POST['connuserlimit'] && !is_numeric($_POST['connuserlimit'])) + $input_errors[] = "The field 'Duplicate Connection Limit' must be numeric."; if (empty($_POST['tunnelnet'])) $input_errors[] = "A 'Tunnel network' must be specified."; @@ -649,6 +652,8 @@ function step12_submitphpaction() { $server['local_network'] = $pconfig['step10']['localnet']; if (isset($pconfig['step10']['concurrentcon'])) $server['maxclients'] = $pconfig['step10']['concurrentcon']; + if (isset($pconfig['step10']['connuserlimit'])) + $server['connlimit'] = $pconfig['step10']['connuserlimit']; if (isset($pconfig['step10']['allowcompression'])) $server['allow_compression'] = $pconfig['step10']['allowcompression']; if (isset($pconfig['step10']['compression'])) diff --git a/src/usr/local/www/wizards/openvpn_wizard.xml b/src/usr/local/www/wizards/openvpn_wizard.xml index cb88ed9ad9f554552d4f643b5583db785b3beb70..8165f92696fbc079a68b0635b9bf273c9e299298 100644 --- a/src/usr/local/www/wizards/openvpn_wizard.xml +++ b/src/usr/local/www/wizards/openvpn_wizard.xml @@ -971,6 +971,14 @@ Allow multiple concurrent connections from clients using the same Common Name.<br/>NOTE: This is not generally recommended, but may be needed for some scenarios. ovpnserver->step10->duplicate_cn + + Duplicate Connection Limit + connuserlimit + Limit the number of concurrent connections from the same user. + input + 10 + ovpnserver->step10->connuserlimit + listtopic Client Settings