Skip to content

Commit daedcf7

Browse files
authored
Merge branch 'opnsense:master' into route-disable-to-enable
2 parents dfd9143 + 913863a commit daedcf7

14 files changed

Lines changed: 244 additions & 138 deletions

File tree

.github/pull_request_template.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
**Important notices**
2+
3+
Before you submit a pull request, we ask you kindly to acknowledge the following:
4+
5+
- [ ] I have read the contributing guidelines at https://github.com/opnsense/core/blob/master/CONTRIBUTING.md
6+
- [ ] I opened an issue first for non-trivial changes and linked it below.
7+
- [ ] AI tools were used to create at least part of the code submitted herewith.
8+
9+
If AI was used, please disclose:
10+
11+
- Model used:
12+
- Extent of AI involvement:
13+
14+
---
15+
16+
**Describe the problem**
17+
18+
A clear and concise description of the problem this pull request addresses.
19+
20+
---
21+
22+
**Describe the proposed solution**
23+
24+
Explain what this pull request changes and why.
25+
26+
---
27+
28+
**Related issue**
29+
30+
If this pull request relates to an issue, link it here.

plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@
640640
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IPPortField.php
641641
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/IntegerField.php
642642
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/InterfaceField.php
643+
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/JsonField.php
643644
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/JsonKeyValueStoreField.php
644645
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/LegacyLinkField.php
645646
/usr/local/opnsense/mvc/app/models/OPNsense/Base/FieldTypes/MacAddressField.php

src/opnsense/mvc/app/controllers/OPNsense/Core/Api/DashboardController.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public function getDashboardAction()
105105
$dashboard = null;
106106

107107
if (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
108-
$dashboard = json_decode(base64_decode($node->dashboard->getValue()), true);
108+
$dashboard = $node->dashboard->deserialize();
109109
}
110110

111111
if (empty($dashboard)) {
@@ -160,12 +160,10 @@ public function saveWidgetsAction()
160160
{
161161
$result = ['result' => 'failed'];
162162
if ($this->request->isPost() && $this->request->hasPost('widgets')) {
163-
$dashboard = json_encode($this->request->getPost());
164-
if (strlen($dashboard) > (1024 * 1024)) {
165-
// prevent saving large blobs of data
166-
throw new UserException(gettext("Dashboard size limit reached"));
167-
} elseif (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
168-
$node->dashboard = base64_encode($dashboard);
163+
if (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
164+
if (!$node->dashboard->serialize($this->request->getPost())) {
165+
throw new UserException(gettext("Dashboard size limit reached"));
166+
}
169167
if ($this->usermdl->serializeToConfig(false, true)) {
170168
/* selectively save dashboard property, ignoring user-config-readonly when set */
171169
Config::getInstance()->save();
@@ -184,7 +182,6 @@ public function restoreDefaultsAction()
184182
if ($this->request->isPost() && ($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
185183
$node->dashboard = '';
186184
$config = Config::getInstance()->object();
187-
$name = $this->getUserName();
188185
if ($this->usermdl->serializeToConfig(false, true)) {
189186
/* selectively reset dashboard property, ignoring user-config-readonly when set */
190187
Config::getInstance()->save();

src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/FilterController.php

Lines changed: 52 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -98,36 +98,16 @@ public function searchRuleAction()
9898
}
9999
}
100100
}
101-
102-
/* filter logic for mvc rules */
103-
$filter_funct_mvc = function ($record) use ($categories, $interfaces, $show_all) {
104-
$is_cat = empty($categories) || array_intersect(explode(',', $record->categories), $categories);
105-
$rule_interfaces = $record->interface->getValues();
106-
107-
// ALL rules, skip interface logic entirely
108-
if ($interfaces === null) {
109-
return $is_cat;
110-
}
111-
112-
if (!$record->interfacenot->isEmpty()) {
113-
$if_intersects = !array_intersect($interfaces, $rule_interfaces); /* All but interface */
114-
} else {
115-
$if_intersects = array_intersect($interfaces, $rule_interfaces);
116-
}
117-
118-
if (empty($interfaces)) {
119-
$is_if = count($rule_interfaces) != 1 || !$record->interfacenot->isEmpty();
120-
} elseif ($show_all) {
121-
$is_if = $if_intersects || empty($rule_interfaces);
122-
} elseif (!$record->interfacenot->isEmpty()) {
123-
// Exclude as it should only be returned with show_all
124-
$is_if = false;
125-
} else {
126-
// Include only an exact match, not a partial overlap
127-
$is_if = $if_intersects && (count($interfaces) == count($rule_interfaces));
101+
/* extract all mvc records so we can filter and sort all at once */
102+
$allrules = [];
103+
foreach ($this->getModel()->rules->rule->iterateItems() as $uuid => $record) {
104+
$row = ['uuid' => $record->getAttributes()['uuid']];
105+
$reflen = strlen($record->__reference) + 1;
106+
foreach ($record->getFlatNodes() as $key => $val) {
107+
$row[substr($key, $reflen)] = $val->getValue();
128108
}
129-
return $is_cat && $is_if;
130-
};
109+
$allrules[] = $row;
110+
}
131111

132112
if ($show_all) {
133113
/* only query stats when fill info is requested */
@@ -136,39 +116,25 @@ public function searchRuleAction()
136116
$rule_stats = [];
137117
}
138118

139-
140-
$filter_funct_rs = function (&$record) use ($categories, $interfaces, $rule_stats) {
141-
/* always merge stats when found */
142-
if (!empty($record['uuid']) && !empty($rule_stats[$record['uuid']])) {
143-
$record = array_merge($record, $rule_stats[$record['uuid']]);
144-
}
145-
/* frontend can format aliases with an alias icon */
146-
foreach (['source_net','source_port','destination_net','destination_port'] as $field) {
147-
if (!empty($record[$field])) {
148-
$record["alias_meta_{$field}"] = $this->getNetworks($record[$field]);
149-
}
150-
}
151-
152-
/* frontend can format categories with colors */
119+
$filter_funct_rs = function (&$record) use ($categories, $interfaces, $rule_stats) {
120+
/* Filter criteria */
153121
$r_categories = !empty($record['categories']) ? array_map('trim', explode(',', $record['categories'])) : [];
154-
$record['category_colors'] = $this->getCategoryColors($r_categories);
155-
156-
if (empty($record['legacy'])) {
157-
/* mvc already filtered */
158-
return true;
159-
}
160122
$is_cat = empty($categories) || array_intersect($r_categories, $categories);
161-
162-
if (!empty($record['interfacenot'])) {
163-
$is_if = !array_intersect(explode(',', $record['interface'] ?? ''), $interfaces ?? []);
123+
$rule_interfaces = array_filter(explode(',', $record['interface'] ?? ''));
124+
if ($interfaces === null || empty($record['interface'])) {
125+
$is_if = true; // ALL interfaces or floating always matches
126+
} elseif (!empty($record['interfacenot'])) {
127+
$is_if = !array_intersect($rule_interfaces, $interfaces ?? []);
164128
} else {
165-
$is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces ?? []);
129+
$is_if = array_intersect($rule_interfaces, $interfaces ?? []);
166130
}
167-
// ALL interfaces or floating always matches
168-
$is_if = $is_if || $interfaces === null || empty($record['interface']);
169131

170-
if ($is_cat && $is_if) {
171-
/* translate/convert legacy fields before returning, similar to mvc handling */
132+
if (!$is_cat || !$is_if) {
133+
return false; /* not reached */
134+
}
135+
136+
/* translate/convert legacy fields before returning, similar to mvc handling */
137+
if (!empty($record['legacy'])) {
172138
foreach ($this->getLegacyFieldMap() as $topic => $data) {
173139
if (!empty($record[$topic])) {
174140
$tmp = [];
@@ -178,42 +144,42 @@ public function searchRuleAction()
178144
$record[$topic] = implode(',', $tmp);
179145
}
180146
}
181-
// Tag legacy rules as "Automatic generated rules" if they have an empty category
182-
if (!empty($record['is_automatic'])) {
183-
$label = gettext('Automatically generated rules');
184-
$record['categories'] = $label; // Grouping key for tree view
185-
$record['category_colors'] = [['name' => $label]]; // Category formatter metadata
186-
}
147+
}
187148

188-
return true;
189-
} else {
190-
return false;
149+
/* Formatting */
150+
151+
/* always merge stats when found */
152+
if (!empty($record['uuid']) && !empty($rule_stats[$record['uuid']])) {
153+
$record = array_merge($record, $rule_stats[$record['uuid']]);
191154
}
192-
};
193155

194-
/**
195-
* XXX: fetch mvc results first, we need to collect all to ensure proper pagination
196-
* as pagination is passed using the request, we need to reset it temporary here as we don't know
197-
* which page we need (yet) and don't want to duplicate large portions of code.
198-
**/
199-
$ORG_REQ = $_REQUEST;
200-
unset($_REQUEST['rowCount']);
201-
unset($_REQUEST['current']);
202-
if ($show_all) {
203-
/* searchBase should not filter here since we later search for IP addresses in aliases */
204-
unset($_REQUEST['searchPhrase']);
205-
}
206-
$filterset = $this->searchBase("rules.rule", null, "sort_order", $filter_funct_mvc)['rows'];
156+
// Tag legacy rules as "Automatic generated rules" if they have an empty category
157+
if (!empty($record['is_automatic'])) {
158+
$label = gettext('Automatically generated rules');
159+
$record['categories'] = $label; // Grouping key for tree view
160+
$record['category_colors'] = [['name' => $label]]; // Category formatter metadata
161+
}
162+
163+
/* frontend can format aliases with an alias icon */
164+
foreach (['source_net','source_port','destination_net','destination_port'] as $field) {
165+
if (!empty($record[$field])) {
166+
$record["alias_meta_{$field}"] = $this->getNetworks($record[$field]);
167+
}
168+
}
169+
170+
/* frontend can format categories with colors */
171+
$record['category_colors'] = $this->getCategoryColors($r_categories);
172+
return true;
173+
};
207174

208175
/* only fetch internal and legacy rules when 'show_all' is set */
209176
if ($show_all) {
210-
$otherrules = json_decode((new Backend())->configdRun('filter list non_mvc_rules'), true) ?? [];
211-
} else {
212-
$otherrules = [];
177+
$allrules = array_merge(
178+
$allrules,
179+
json_decode((new Backend())->configdRun('filter list non_mvc_rules'), true) ?? []
180+
);
213181
}
214182

215-
$_REQUEST = $ORG_REQ; /* XXX: fix me ?*/
216-
217183
$search_clauses = [];
218184
$backend = new Backend();
219185
foreach (preg_split('/\s+/', (string)$this->request->getPost('searchPhrase', null, '')) as $token) {
@@ -229,7 +195,7 @@ public function searchRuleAction()
229195
}
230196
}
231197
$result = $this->searchRecordsetBase(
232-
array_merge($otherrules, $filterset),
198+
$allrules,
233199
null,
234200
"sort_order",
235201
$filter_funct_rs,

src/opnsense/mvc/app/library/OPNsense/Auth/LDAP.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ public function connect($bind_url, $userdn = null, $password = null, $timeout =
392392

393393
/**
394394
* search user by name or expression
395-
* @param string $username username(s) to search
395+
* @param string $username username(s) to search (unescaped ldap search)
396396
* @param string $userNameAttribute ldap attribute to use for the search
397397
* @param string|null $extendedQuery additional search criteria (narrow down search)
398398
* @return array|bool
@@ -405,12 +405,11 @@ public function searchUsers($username, $userNameAttribute, $extendedQuery = null
405405
// add $userNameAttribute to search results
406406
$this->addSearchAttribute($userNameAttribute);
407407
$result = [];
408-
$username_safe = ldap_escape($username, '', LDAP_ESCAPE_FILTER);
409408
if (empty($extendedQuery)) {
410-
$searchResults = $this->search("({$userNameAttribute}={$username_safe})");
409+
$searchResults = $this->search("({$userNameAttribute}={$username})");
411410
} else {
412411
// add additional search phrases
413-
$searchResults = $this->search("(&({$userNameAttribute}={$username_safe})({$extendedQuery}))");
412+
$searchResults = $this->search("(&({$userNameAttribute}={$username})({$extendedQuery}))");
414413
}
415414
if ($searchResults !== false) {
416415
for ($i = 0; $i < $searchResults["count"]; $i++) {
@@ -509,7 +508,8 @@ protected function _authenticate($username, $password)
509508
} else {
510509
// we don't know this users distinguished name, try to find it
511510
if ($this->connect($this->ldapBindURL, $this->ldapBindDN, $this->ldapBindPassword)) {
512-
$result = $this->searchUsers($username, $this->ldapAttributeUser, $this->ldapExtendedQuery);
511+
$username_safe = ldap_escape($username, '', LDAP_ESCAPE_FILTER);
512+
$result = $this->searchUsers($username_safe, $this->ldapAttributeUser, $this->ldapExtendedQuery);
513513
if ($result !== false && count($result) > 0) {
514514
$user_dn = $result[0]['dn'];
515515
$ldap_is_connected = $this->connect($this->ldapBindURL, $result[0]['dn'], $password);

src/opnsense/mvc/app/library/OPNsense/Auth/Radius.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/*
4-
* Copyright (C) 2015 Deciso B.V.
4+
* Copyright (C) 2015-2026 Deciso B.V.
55
* All rights reserved.
66
*
77
* Redistribution and use in source and binary forms, with or without
@@ -28,6 +28,8 @@
2828

2929
namespace OPNsense\Auth;
3030

31+
use OPNsense\Firewall\Util;
32+
3133
/**
3234
* Class Radius connector
3335
* @package OPNsense\Auth
@@ -69,6 +71,11 @@ class Radius extends Base implements IAuthConnector
6971
*/
7072
private $callingStationId = null;
7173

74+
/**
75+
* @var string ip addess to use for NAS-IP-Address attribute
76+
*/
77+
private $nasIpAddress = null;
78+
7279
/**
7380
* @var int timeout to use
7481
*/
@@ -158,15 +165,17 @@ public function getDescription()
158165
public function setProperties($config)
159166
{
160167
// map properties to object
161-
$confMap = array('host' => 'radiusHost',
162-
'radius_secret' => 'sharedSecret',
163-
'radius_timeout' => 'timeout',
164-
'radius_auth_port' => 'authPort',
168+
$confMap = [
169+
'host' => 'radiusHost',
165170
'radius_acct_port' => 'acctPort',
171+
'radius_auth_port' => 'authPort',
172+
'radius_nasipaddress' => 'nasIpAddress',
166173
'radius_protocol' => 'protocol',
174+
'radius_secret' => 'sharedSecret',
167175
'radius_stationid' => 'calledStationId',
168-
'refid' => 'nasIdentifier'
169-
);
176+
'radius_timeout' => 'timeout',
177+
'refid' => 'nasIdentifier',
178+
];
170179

171180
// map properties 1-on-1
172181
foreach ($confMap as $confSetting => $objectProperty) {
@@ -195,6 +204,7 @@ public function setProperties($config)
195204
public function getConfigurationOptions()
196205
{
197206
$options = [];
207+
198208
$options['radius_protocol'] = [];
199209
$options['radius_protocol']['name'] = gettext('Protocol');
200210
$options['radius_protocol']['type'] = 'dropdown';
@@ -210,6 +220,19 @@ public function getConfigurationOptions()
210220
return [];
211221
}
212222
};
223+
224+
$options['radius_nasipaddress'] = [];
225+
$options['radius_nasipaddress']['name'] = gettext('NAS IP address');
226+
$options['radius_nasipaddress']['type'] = 'text';
227+
$options['radius_nasipaddress']['default'] = '';
228+
$options['radius_nasipaddress']['validate'] = function ($value) {
229+
if (!empty($value) && !Util::isIpAddress($value)) {
230+
return [gettext('Invalid NAS IP address specified')];
231+
} else {
232+
return [];
233+
}
234+
};
235+
213236
return $options;
214237
}
215238

@@ -501,18 +524,20 @@ public function authenticate($username, $password)
501524
$error = radius_strerror($radius);
502525
} elseif (!radius_put_int($radius, RADIUS_SERVICE_TYPE, RADIUS_LOGIN)) {
503526
$error = radius_strerror($radius);
504-
} elseif (!radius_put_int($radius, RADIUS_FRAMED_PROTOCOL, RADIUS_ETHERNET)) {
527+
} elseif (empty($this->nasIpAddress) && !radius_put_int($radius, RADIUS_FRAMED_PROTOCOL, RADIUS_ETHERNET)) {
505528
$error = radius_strerror($radius);
506529
} elseif (!radius_put_string($radius, RADIUS_NAS_IDENTIFIER, $this->nasIdentifier)) {
507530
$error = radius_strerror($radius);
508531
} elseif (!radius_put_int($radius, RADIUS_NAS_PORT, 0)) {
509532
$error = radius_strerror($radius);
510-
} elseif (!radius_put_int($radius, RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET)) {
533+
} elseif (!radius_put_int($radius, RADIUS_NAS_PORT_TYPE, empty($this->nasIpAddress) ? RADIUS_ETHERNET : RADIUS_VIRTUAL)) {
511534
$error = radius_strerror($radius);
512535
} elseif (!empty($this->calledStationId) && !radius_put_string($radius, RADIUS_CALLED_STATION_ID, $this->calledStationId)) {
513536
$error = radius_strerror($radius);
514537
} elseif (!empty($this->callingStationId) && !radius_put_string($radius, RADIUS_CALLING_STATION_ID, $this->callingStationId)) {
515538
$error = radius_strerror($radius);
539+
} elseif (!empty($this->nasIpAddress) && !radius_put_addr($radius, RADIUS_NAS_IP_ADDRESS, $this->nasIpAddress)) {
540+
$error = radius_stderror($radius);
516541
} else {
517542
// Implement extra protocols in this section.
518543
switch ($this->protocol) {

0 commit comments

Comments
 (0)