diff --git a/src/etc/inc/globals.inc b/src/etc/inc/globals.inc index 304bfee7f3..53b876ed85 100644 --- a/src/etc/inc/globals.inc +++ b/src/etc/inc/globals.inc @@ -74,7 +74,7 @@ $g = array( "disablecrashreporter" => false, "crashreporterurl" => "https://crashreporter.pfsense.org/crash_reporter.php", "debug" => false, - "latest_config" => "17.3", + "latest_config" => "17.4", "minimum_ram_warning" => "101", "minimum_ram_warning_text" => "128 MB", "wan_interface_name" => "wan", diff --git a/src/etc/inc/upgrade_config.inc b/src/etc/inc/upgrade_config.inc index 6b7ac30397..0236d7e744 100644 --- a/src/etc/inc/upgrade_config.inc +++ b/src/etc/inc/upgrade_config.inc @@ -5505,4 +5505,23 @@ function additional_config_upgrade() { global $config; } +/* IPsec Phase1 now supports multiple authentication ciphers to be specified from the webgui. + * This is usefull for mobile users using different OS's supporting different ciphers. + */ +function upgrade_173_to_174() { + global $config; + if (is_array($config['ipsec']['phase1'])) { + $a_phase1 = &$config['ipsec']['phase1']; + foreach($a_phase1 as &$phase1) { + $item = array(); + $item['encryption-algorithm'] = $phase1['encryption-algorithm']; + $item['hash-algorithm'] = $phase1['hash-algorithm']; + $item['dhgroup'] = $phase1['dhgroup']; + $phase1['encryption']['item'][] = $item; + unset($phase1['encryption-algorithm']); + unset($phase1['hash-algorithm']); + unset($phase1['dhgroup']); + } + } +} ?> diff --git a/src/etc/inc/vpn.inc b/src/etc/inc/vpn.inc index 58af30c64a..3df157a9db 100644 --- a/src/etc/inc/vpn.inc +++ b/src/etc/inc/vpn.inc @@ -951,22 +951,30 @@ EOD; } } - if (is_array($ph1ent['encryption-algorithm']) && !empty($ph1ent['encryption-algorithm']['name']) && !empty($ph1ent['hash-algorithm'])) { - $ealgosp1 = ''; - $ealg_id = $ph1ent['encryption-algorithm']['name']; - $ealg_kl = $ph1ent['encryption-algorithm']['keylen']; - if ($ealg_kl) { - $ealgosp1 = "ike = {$ealg_id}{$ealg_kl}-{$ph1ent['hash-algorithm']}"; - } else { - $ealgosp1 = "ike = {$ealg_id}-{$ph1ent['hash-algorithm']}"; - } + $ealgosp1 = ''; + if (is_array($ph1ent['encryption']['item'])) { + $ciphers = ""; + foreach($ph1ent['encryption']['item'] as $p1enc) { + if (!is_array($p1enc['encryption-algorithm']) || + empty($p1enc['encryption-algorithm']['name']) || + empty($p1enc['hash-algorithm'])) { + continue; + } + $ciphers .= ","; + $ciphers .= $p1enc['encryption-algorithm']['name']; + $ealg_kl = $p1enc['encryption-algorithm']['keylen']; + if ($ealg_kl) { + $ciphers .= "{$ealg_kl}"; + } + $ciphers .= "-{$p1enc['hash-algorithm']}"; - $modp = vpn_ipsec_convert_to_modp($ph1ent['dhgroup']); - if (!empty($modp)) { - $ealgosp1 .= "-{$modp}"; + $modp = vpn_ipsec_convert_to_modp($p1enc['dhgroup']); + if (!empty($modp)) { + $ciphers .= "-{$modp}"; + } } - - $ealgosp1 .= "!"; + $ciphers = substr($ciphers, 1); + $ealgosp1 = "ike = {$ciphers}!"; } if ($ph1ent['dpd_delay'] && $ph1ent['dpd_maxfail']) { diff --git a/src/usr/local/www/classes/Form/ListItem.class.php b/src/usr/local/www/classes/Form/ListItem.class.php new file mode 100644 index 0000000000..6b1d0396ad --- /dev/null +++ b/src/usr/local/www/classes/Form/ListItem.class.php @@ -0,0 +1,186 @@ + array('form-listitem' => true), + ); + protected $_title; + protected $_inputs = array(); + protected $_labelTarget; + protected $_help; + protected $_helpParams = array(); + + public function __construct($title) + { + $this->_title = $title; + } + + public function add(Form_Element $input) + { + if ($input instanceof Form_Input) { + $group = new Form_Group($input->getTitle()); + $group->add($input); + $input = $group; + } + + + array_push($this->_inputs, $input); + + return $input; + } + + public function setLabelTarget(Form_Input $input) + { + $this->_labelTarget = $input; + } + + public function setHelp() + { + $args = func_get_args(); + $arg0_len = strlen($args[0]); + + if (($arg0_len > 0) && ($arg0_len < 4096)) { + $args[0] = gettext($args[0]); + } + + if (func_num_args() == 1) { + $this->_help = $args[0]; + } else { + $this->_help = call_user_func_array('sprintf', $args); + } + + $this->_helpParams = ""; + + return $this; + } + + public function enableDuplication($max = null, $horiz = false) + { + if ($horiz) + $this->addClass('user-duplication-horiz'); // added buttons are 2 cols wide with no offset + else + $this->addClass('user-duplication'); // added buttons 10 cols wide with 2 col offset + + if (isset($max)) + $this->_attributes('data-duplicate-max', $max); + + foreach ($this->_inputs as $input) { + if ($input instanceof Form_Input) + $input->setIsRepeated(); + } + + return $this; + } + + protected function _getHelp() + { + if (empty($this->_help)) + return null; + + $group = new Form_Element; + $group->addClass('col-sm-'. Form::MAX_INPUT_WIDTH, 'col-sm-offset-'. Form::LABEL_WIDTH); + + $help = $this->_help; + + return << + {$help} + + +EOT; + } + + public function __toString() + { + global $config, $user_settings; + + $element = Form_Element::__toString(); + + // Automatically determine width for inputs without explicit set + $spaceLeft = Form::MAX_INPUT_WIDTH; + $missingWidth = array(); + + foreach ($this->_inputs as $input) + { + if ($input instanceof Form_Input) { + $width = $input->getWidth(); + } else { + unset($width); + } + if (isset($width)) + $spaceLeft -= $width; + else + array_push($missingWidth, $input); + } + + if ($this->_labelTarget instanceof Form_Input) { + if (strtolower($this->_labelTarget->getType()) == 'hidden') { + $hidden = true; + } + + $form_controls = array('input', 'select', 'button', 'textarea', 'option', 'optgroup', 'fieldset', 'label'); + + if (in_array(strtolower($this->_labelTarget->getTagName()), $form_controls) && !$hidden) { + $target = $this->_labelTarget->getId(); + } + } + $inputs = implode('', $this->_inputs); + $help = $this->_getHelp(); + + if (!$user_settings['webgui']['webguileftcolumnhyper']) { + $target = null; + } + + if (!empty(trim($this->_title)) || is_numeric($this->_title)) { + $title = htmlspecialchars(gettext($this->_title)); + + // If the element tile (label) begins with a '*', remove the '*' and add a span with class + // 'element-required'. Text decoration can then be added in the CSS to indicate that this is a + // required field + if (substr($title, 0, 1 ) === "*" ) { + $title = '' . substr($title, 1) . ''; + } else { + $title = '' . $title . ''; + } + } + + /*return << + {$inputs} + +EOT;*/ + + return << + {$inputs} + {$help} + +EOT; + } +} diff --git a/src/usr/local/www/css/Compact-RED.css b/src/usr/local/www/css/Compact-RED.css index 1118caaa3a..d32724a93b 100644 --- a/src/usr/local/www/css/Compact-RED.css +++ b/src/usr/local/www/css/Compact-RED.css @@ -128,6 +128,11 @@ body { .form-group { padding: 2px 5px 2px 5px; } + +.user-duplication .controls { + margin-top: 0px; +} + .table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th { padding:2px; } diff --git a/src/usr/local/www/css/pfSense.css b/src/usr/local/www/css/pfSense.css index bc8dc264a2..03ac1d9e29 100644 --- a/src/usr/local/www/css/pfSense.css +++ b/src/usr/local/www/css/pfSense.css @@ -303,6 +303,13 @@ tr.disabled a { border-bottom: none; } +.form-listitem { + border-top: 3px solid #E0E0E0; +} +.form-listitem:first-child { + border-top: none; +} + .input-group-addon { padding: 0 12px; } diff --git a/src/usr/local/www/js/pfSense.js b/src/usr/local/www/js/pfSense.js index bfae83f016..1ab5f39d82 100644 --- a/src/usr/local/www/js/pfSense.js +++ b/src/usr/local/www/js/pfSense.js @@ -113,6 +113,37 @@ $(function() { }); })(); + // Add +/- buttons to certain Groups; to allow adding multiple entries + (function() + { + var groups = $('div.form-listitem.user-duplication'); + var fg = $('
'); + var controlsContainer = $('
'); + var plus = $('Add'); + var minus = $('Delete'); + + minus.on('click', function(){ + var groups = $('div.form-listitem.user-duplication'); + if (groups.length > 1) { + $(this).parents('div.form-listitem').remove(); + } + }); + + plus.on('click', function(){ + var group = $(this).parents('div.form-listitem'); + var clone = group.clone(true); + bump_input_id(clone); + clone.appendTo(group.parent()); + }); + + groups.each(function(idx, group){ + var fgClone = fg.clone(true).appendTo(group); + var controlsClone = controlsContainer.clone(true).appendTo(fgClone); + minus.clone(true).appendTo(controlsClone); + plus.clone(true).appendTo(controlsClone); + }); + })(); + // Automatically change IpAddress mask selectors to 128/32 options for IPv6/IPv4 addresses $('span.pfIpMask + select').each(function (idx, select){ var input = $(select).prevAll('input[type=text]'); diff --git a/src/usr/local/www/js/pfSenseHelpers.js b/src/usr/local/www/js/pfSenseHelpers.js index 7b41aa036b..36234bbce5 100644 --- a/src/usr/local/www/js/pfSenseHelpers.js +++ b/src/usr/local/www/js/pfSenseHelpers.js @@ -219,6 +219,12 @@ function moveHelpText(id) { }); } +// Increment the number at the end of the string +function getStringInt( str ) { + var data = str.match(/(\D*)(\d+)(\D*)/), newStr = ""; + return Number( data[ 2 ] ); +} + // Increment the number at the end of the string function bumpStringInt( str ) { var data = str.match(/(\D*)(\d+)(\D*)/), newStr = ""; @@ -293,23 +299,7 @@ function checkLastRow() { } } -function add_row() { - // Find the last repeatable group - var lastRepeatableGroup = $('.repeatable:last'); - - // If the number of repeats exceeds the maximum, do not add another clone - if ($('.repeatable').length >= lastRepeatableGroup.attr('max_repeats')) { - // Alert user if alert message is specified - if (typeof lastRepeatableGroup.attr('max_repeats_alert') !== 'undefined') { - alert(lastRepeatableGroup.attr('max_repeats_alert')); - } - return; - } - - // Clone it - var newGroup = lastRepeatableGroup.clone(); - - // Increment the suffix number for each input element in the new group +function bump_input_id(newGroup) { $(newGroup).find('input').each(function() { $(this).prop("id", bumpStringInt(this.id)); $(this).prop("name", bumpStringInt(this.name)); @@ -339,6 +329,25 @@ function add_row() { } } }); +} +function add_row() { + // Find the last repeatable group + var lastRepeatableGroup = $('.repeatable:last'); + + // If the number of repeats exceeds the maximum, do not add another clone + if ($('.repeatable').length >= lastRepeatableGroup.attr('max_repeats')) { + // Alert user if alert message is specified + if (typeof lastRepeatableGroup.attr('max_repeats_alert') !== 'undefined') { + alert(lastRepeatableGroup.attr('max_repeats_alert')); + } + return; + } + + // Clone it + var newGroup = lastRepeatableGroup.clone(); + + // Increment the suffix number for each input element in the new group + bump_input_id(newGroup); // And for "for" tags // $(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for'))); diff --git a/src/usr/local/www/vpn_ipsec.php b/src/usr/local/www/vpn_ipsec.php index df1e6e96ab..d8393fb0e9 100644 --- a/src/usr/local/www/vpn_ipsec.php +++ b/src/usr/local/www/vpn_ipsec.php @@ -252,6 +252,7 @@ if (is_subsystem_dirty('ipsec')) { + @@ -334,19 +335,51 @@ $i = 0; foreach ($a_phase1 as $ph1ent): - "; + } + echo $p1_ealgos[$p1algo['encryption-algorithm']['name']]['name']; + if ($p1algo['encryption-algorithm']['keylen']) { + if ($p1algo['encryption-algorithm']['keylen'] == "auto") { + echo " (" . gettext("auto") . ")"; + } else { + echo " ({$p1algo['encryption-algorithm']['keylen']} " . gettext("bits") . ")"; + } + } + $first = false; + } } - } ?> - +"; + } + echo $p1_halgos[$p1algo['hash-algorithm']]; + $first = false; + } + } + ?> + + +"; + } + echo str_replace(" "," ",$p1_dhgroups[$p1algo['dhgroup']]); + $first = false; + } + } + ?> diff --git a/src/usr/local/www/vpn_ipsec_phase1.php b/src/usr/local/www/vpn_ipsec_phase1.php index aec75d42d7..6faf12212e 100644 --- a/src/usr/local/www/vpn_ipsec_phase1.php +++ b/src/usr/local/www/vpn_ipsec_phase1.php @@ -91,9 +91,7 @@ if (isset($p1index) && $a_phase1[$p1index]) { $pconfig['myid_data'] = $a_phase1[$p1index]['myid_data']; $pconfig['peerid_type'] = $a_phase1[$p1index]['peerid_type']; $pconfig['peerid_data'] = $a_phase1[$p1index]['peerid_data']; - $pconfig['ealgo'] = $a_phase1[$p1index]['encryption-algorithm']; - $pconfig['halgo'] = $a_phase1[$p1index]['hash-algorithm']; - $pconfig['dhgroup'] = $a_phase1[$p1index]['dhgroup']; + $pconfig['encryption'] = $a_phase1[$p1index]['encryption']; $pconfig['lifetime'] = $a_phase1[$p1index]['lifetime']; $pconfig['authentication_method'] = $a_phase1[$p1index]['authentication_method']; @@ -153,9 +151,6 @@ if (isset($p1index) && $a_phase1[$p1index]) { $pconfig['myid_type'] = "myaddress"; $pconfig['peerid_type'] = "peeraddress"; $pconfig['authentication_method'] = "pre_shared_key"; - $pconfig['ealgo'] = array(name => "aes"); - $pconfig['halgo'] = "sha1"; - $pconfig['dhgroup'] = "2"; $pconfig['lifetime'] = "28800"; $pconfig['rekey_enable'] = true; $pconfig['nat_traversal'] = 'on'; @@ -169,6 +164,14 @@ if (isset($p1index) && $a_phase1[$p1index]) { $pconfig['mode'] = "aggressive"; } } +// default value for new P1 and failsafe to always have at least 1 encryption item for the Form_ListItem +if (!is_array($pconfig['encryption']['item']) || count($pconfig['encryption']['item']) == 0) { + $item = array(); + $item['encryption-algorithm'] = array(name => "aes"); + $item['hash-algorithm'] = "sha1"; + $item['dhgroup'] = "2"; + $pconfig['encryption']['item'][] = $item; +} if (isset($_REQUEST['dup']) && is_numericint($_REQUEST['dup'])) { unset($p1index); @@ -178,6 +181,17 @@ if ($_POST['save']) { unset($input_errors); $pconfig = $_POST; + for($i = 0; $i < 100; $i++) { + if (isset($_POST['ealgo_algo'.$i])) { + $item = array(); + $item['encryption-algorithm']['name'] = $_POST['ealgo_algo'.$i]; + $item['encryption-algorithm']['keylen'] = $_POST['ealgo_keylen'.$i]; + $item['hash-algorithm'] = $_POST['halgo'.$i]; + $item['dhgroup'] = $_POST['dhgroup'.$i]; + $pconfig['encryption']['item'][] = $item; + } + } + /* input validation */ $method = $pconfig['authentication_method']; @@ -407,11 +421,12 @@ if ($_POST['save']) { if (!empty($pconfig['iketype']) && $pconfig['iketype'] != "ikev1" && $pconfig['iketype'] != "ikev2" && $pconfig['iketype'] != "auto") { $input_errors[] = gettext("Valid arguments for IKE type are v1, v2 or auto"); } - - if (preg_match("/aes\d+gcm/", $_POST['ealgo']) && $_POST['iketype'] != "ikev2") { - $input_errors[] = gettext("Encryption Algorithm AES-GCM can only be used with IKEv2"); + + foreach($pconfig['encryption']['item'] as $p1algo) { + if (preg_match("/aes\d+gcm/", $p1algo['encryption-algorithm']['name']) && $_POST['iketype'] != "ikev2") { + $input_errors[] = gettext("Encryption Algorithm AES-GCM can only be used with IKEv2"); + } } - /* auth backend for mobile eap-radius VPNs should be a RADIUS server */ if (($pconfig['authentication_method'] == 'eap-radius') && $pconfig['mobile']) { if (!empty($config['ipsec']['client']['user_source'])) { @@ -425,13 +440,6 @@ if ($_POST['save']) { } } - /* build our encryption algorithms array */ - $pconfig['ealgo'] = array(); - $pconfig['ealgo']['name'] = $_POST['ealgo']; - if ($pconfig['ealgo_keylen']) { - $pconfig['ealgo']['keylen'] = $_POST['ealgo_keylen']; - } - if (!$input_errors) { $ph1ent['ikeid'] = $pconfig['ikeid']; $ph1ent['iketype'] = $pconfig['iketype']; @@ -463,9 +471,7 @@ if ($_POST['save']) { $ph1ent['peerid_type'] = $pconfig['peerid_type']; $ph1ent['peerid_data'] = $pconfig['peerid_data']; - $ph1ent['encryption-algorithm'] = $pconfig['ealgo']; - $ph1ent['hash-algorithm'] = $pconfig['halgo']; - $ph1ent['dhgroup'] = $pconfig['dhgroup']; + $ph1ent['encryption'] = $pconfig['encryption']; $ph1ent['lifetime'] = $pconfig['lifetime']; $ph1ent['pre-shared-key'] = $pconfig['pskey']; $ph1ent['private-key'] = base64_encode($pconfig['privatekey']); @@ -799,40 +805,51 @@ $section->addInput(new Form_Select( ))->setHelp('Select a certificate authority previously configured in the Certificate Manager.'); $form->add($section); +$section = new Form_Section('NOTITLE'); +foreach($pconfig['encryption']['item'] as $key => $p1enc) { + $li = new Form_ListItem(""); + $group = new Form_Group('*Encryption Algorithm'); + $group->add(new Form_Select( + 'ealgo_algo'.$key, + null, + $p1enc['encryption-algorithm']['name'], + build_eal_list() + )); + $group->add(new Form_Select( + 'ealgo_keylen'.$key, + null, + $p1enc['encryption-algorithm']['keylen'], + array() + )); + $li->add($group); -$section = new Form_Section('Phase 1 Proposal (Algorithms)'); + $li->add(new Form_Select( + 'halgo'.$key, + '*Hash Algorithm', + $p1enc['hash-algorithm'], + $p1_halgos + ))->setHelp('Must match the setting chosen on the remote side.'); -$group = new Form_Group('*Encryption Algorithm'); + $li->add(new Form_Select( + 'dhgroup'.$key, + '*DH Group', + $p1enc['dhgroup'], + $p1_dhgroups + ))->setHelp('Must match the setting chosen on the remote side.'); -$group->add(new Form_Select( - 'ealgo', + $li->enableDuplication(null); + $section->add($li); +} +$form->add($section); + +$section = new Form_Section('NOTITLE'); +$btnaddopt = new Form_Button( + 'algoaddrow', + 'Add Encryption Settings', null, - $pconfig['ealgo']['name'], - build_eal_list() -)); - -$group->add(new Form_Select( - 'ealgo_keylen', - null, - $pconfig['ealgo_keylen'], - array() -)); - -$section->add($group); - -$section->addInput(new Form_Select( - 'halgo', - '*Hash Algorithm', - $pconfig['halgo'], - $p1_halgos -))->setHelp('Must match the setting chosen on the remote side.'); - -$section->addInput(new Form_Select( - 'dhgroup', - '*DH Group', - $pconfig['dhgroup'], - $p1_dhgroups -))->setHelp('Must match the setting chosen on the remote side.'); + 'fa-plus' +); +$btnaddopt->removeClass('btn-primary')->addClass('btn-success btn-sm'); $section->addInput(new Form_Input( 'lifetime', @@ -964,13 +981,6 @@ $form->add($section); print($form); -/* determine if we should init the key length */ -$keyset = ''; -if (isset($pconfig['ealgo']['keylen'])) { - if (is_numericint($pconfig['ealgo']['keylen'])) { - $keyset = $pconfig['ealgo']['keylen']; - } -} ?> @@ -979,6 +989,19 @@ if (isset($pconfig['ealgo']['keylen'])) {