diff --git a/plist b/plist
index 1cc03e991..11ab1c638 100644
--- a/plist
+++ b/plist
@@ -360,6 +360,7 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogSPD.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/settings.xml
+/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/BridgeSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/GifSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/GreSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/LaggSettingsController.php
@@ -369,6 +370,7 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VipSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VlanSettingsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/VxlanSettingsController.php
+/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/BridgeController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/GifController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/GreController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/LaggController.php
@@ -378,6 +380,7 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VipController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VlanController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/VxlanController.php
+/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogBridge.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogGif.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogGre.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogLagg.xml
@@ -752,6 +755,9 @@
/usr/local/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php
/usr/local/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/ACL/ACL.xml
+/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.php
+/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.xml
+/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/BridgeMemberField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/LaggInterfaceField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/LinkAddressField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/NeighborField.php
@@ -915,6 +921,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/IPsec/spd.volt
/usr/local/opnsense/mvc/app/views/OPNsense/IPsec/tunnels.volt
/usr/local/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt
+/usr/local/opnsense/mvc/app/views/OPNsense/Interface/bridge.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/gif.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/gre.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Interface/lagg.volt
@@ -1175,6 +1182,7 @@
/usr/local/opnsense/scripts/interfaces/ppp-linkup.sh
/usr/local/opnsense/scripts/interfaces/ppp-rename.sh
/usr/local/opnsense/scripts/interfaces/ppp-uptime.sh
+/usr/local/opnsense/scripts/interfaces/reconfigure_bridges.php
/usr/local/opnsense/scripts/interfaces/reconfigure_gifs.php
/usr/local/opnsense/scripts/interfaces/reconfigure_gres.php
/usr/local/opnsense/scripts/interfaces/reconfigure_laggs.php
@@ -2404,8 +2412,6 @@
/usr/local/www/index.php
/usr/local/www/interfaces.php
/usr/local/www/interfaces_assign.php
-/usr/local/www/interfaces_bridge.php
-/usr/local/www/interfaces_bridge_edit.php
/usr/local/www/interfaces_ppps.php
/usr/local/www/interfaces_ppps_edit.php
/usr/local/www/interfaces_wireless.php
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/BridgeSettingsController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/BridgeSettingsController.php
new file mode 100644
index 000000000..941cc7b6a
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/Api/BridgeSettingsController.php
@@ -0,0 +1,138 @@
+searchBase("bridged", null, "descr");
+ }
+
+ /**
+ * Update bridge with given properties
+ * @param string $uuid internal id
+ * @return array save result + validation output
+ */
+ public function setItemAction($uuid)
+ {
+ Config::getInstance()->lock();
+ $node = $this->getModel()->getNodeByReference('bridged.' . $uuid);
+ $overlay = null;
+ if (!empty($node)) {
+ // not allowed to change bridge interface name
+ $overlay['bridgeif'] = (string)$node->bridgeif;
+ }
+ return $this->setBase("bridge", "bridged", $uuid, $overlay);
+ }
+
+ /**
+ * Add new bridge and set with attributes from post
+ * @return array save result + validation output
+ */
+ public function addItemAction()
+ {
+ Config::getInstance()->lock();
+ $overlay = [];
+ $ifnames = [];
+ foreach ($this->getModel()->bridged->iterateItems() as $node) {
+ $ifnames[] = (string)$node->bridgeif;
+ }
+ for ($i = 0; true; ++$i) {
+ $gifif = sprintf('bridge%d', $i);
+ if (!in_array($gifif, $ifnames)) {
+ $overlay['bridgeif'] = $gifif;
+ break;
+ }
+ }
+
+ return $this->addBase("bridge", "bridged", $overlay);
+ }
+
+ /**
+ * Retrieve bridge settings or return defaults for new one
+ * @param $uuid item unique id
+ * @return array bridge content
+ */
+ public function getItemAction($uuid = null)
+ {
+ return $this->getBase("bridge", "bridged", $uuid);
+ }
+
+ /**
+ * Delete bridge by uuid
+ * @param string $uuid internal id
+ * @return array save status
+ */
+ public function delItemAction($uuid)
+ {
+ Config::getInstance()->lock();
+ $node = $this->getModel()->getNodeByReference('bridged.' . $uuid);
+ if ($node != null) {
+ $cfg = Config::getInstance()->object();
+ foreach ($cfg->interfaces->children() as $key => $value) {
+ if ((string)$value->if == (string)$node->bridgeif) {
+ throw new \OPNsense\Base\UserException(
+ sprintf(gettext("Cannot delete bridge. Currently in use by [%s] %s"), $key, $value),
+ gettext("bridge in use")
+ );
+ }
+ }
+ }
+ return $this->delBase("bridged", $uuid);
+ }
+
+ /**
+ * reconfigure bridges
+ */
+ public function reconfigureAction()
+ {
+ if ($this->request->isPost()) {
+ (new Backend())->configdRun("interface bridge configure");
+ return ["status" => "ok"];
+ } else {
+ return ["status" => "failed"];
+ }
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/BridgeController.php b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/BridgeController.php
new file mode 100644
index 000000000..9b3e78109
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/BridgeController.php
@@ -0,0 +1,40 @@
+view->pick('OPNsense/Interface/bridge');
+
+ $this->view->formDialogBridge = $this->getForm("dialogBridge");
+ $this->view->formGridBridge = $this->getFormGrid("dialogBridge");
+ }
+}
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogBridge.xml b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogBridge.xml
new file mode 100644
index 000000000..a8ddf119a
--- /dev/null
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Interfaces/forms/dialogBridge.xml
@@ -0,0 +1,194 @@
+
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 0c0cf7768..9ed304f45 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
@@ -299,17 +299,12 @@
- Interfaces: Bridge edit
-
- interfaces_bridge_edit.php*
-
-
-
Interfaces: Bridge
- interfaces_bridge.php*
+ ui/interfaces/bridge
+ api/interfaces/bridge_settings/*
-
+
Interfaces: GIF
diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.php
new file mode 100644
index 000000000..d3b67c039
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.php
@@ -0,0 +1,76 @@
+bridged->iterateItems() as $bridge) {
+ if (!$validateFullModel && !$bridge->isFieldChanged()) {
+ continue;
+ }
+ $key = $bridge->__reference;
+ $members = explode(',', $bridge->members->getCurrentValue());
+ if (!$bridge->span->isEmpty() && in_array($bridge->span->getCurrentValue(), $members)) {
+ $messages->appendMessage(
+ new Message(
+ gettext("Span interface cannot be part of the bridge."),
+ $key . ".span"
+ )
+ );
+ }
+ foreach (['stp', 'edge', 'autoedge', 'ptp', 'autoptp', 'static', 'private'] as $section) {
+ if ($bridge->$section->isEmpty()) {
+ continue;
+ }
+ foreach (explode(',', $bridge->$section->getCurrentValue()) as $if) {
+ if (!in_array($if, $members)) {
+ $messages->appendMessage(
+ new Message(
+ gettext("Contains non bridge members."),
+ $key . "." . $section
+ )
+ );
+ break;
+ }
+ }
+ }
+ }
+ return $messages;
+ }
+}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.xml b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.xml
new file mode 100644
index 000000000..ec3adcf27
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Bridge.xml
@@ -0,0 +1,74 @@
+
+ /bridges
+ 1.0.0
+ Bridge interfaces
+
+
+
+ Y
+
+
+ Bridge already exists!
+ UniqueConstraint
+
+
+ /^bridge[\d]+$/
+
+
+ Y
+ Y
+
+
+
+
+ Y
+ rstp
+
+ RSTP
+ STP
+
+
+
+ Y
+
+
+ 6
+ 40
+
+
+ 4
+ 30
+
+
+ 1
+ 10
+
+
+ 1
+
+
+ 0
+
+
+
+ Y
+
+
+ Y
+
+
+ Y
+
+
+ Y
+
+
+ Y
+
+
+ Y
+
+
+
+
+
diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/BridgeMemberField.php b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/BridgeMemberField.php
new file mode 100644
index 000000000..968f9bf7a
--- /dev/null
+++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/FieldTypes/BridgeMemberField.php
@@ -0,0 +1,58 @@
+object();
+ if (!empty($configHandle->interfaces)) {
+ foreach ($configHandle->interfaces->children() as $ifname => $node) {
+ if (!empty((string)$node->virtual)) {
+ continue;
+ } elseif (!empty((string)$node->if) && str_starts_with('gre', $node->if)) {
+ continue;
+ } elseif (!empty((string)$node->if) && str_starts_with('lo', $node->if)) {
+ continue;
+ }
+ $descr = !empty((string)$node->descr) ? (string)$node->descr : strtoupper($ifname);
+ self::$interfaces[$ifname] = $descr;
+ }
+ }
+ }
+ $this->internalOptionList = self::$interfaces;
+ return parent::actionPostLoadingEvent();
+ }
+}
diff --git a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml
index b838bb307..c3d63c74c 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml
+++ b/src/opnsense/mvc/app/models/OPNsense/Interfaces/Menu/Menu.xml
@@ -2,9 +2,7 @@
-
-
-
+
diff --git a/src/opnsense/mvc/app/views/OPNsense/Interface/bridge.volt b/src/opnsense/mvc/app/views/OPNsense/Interface/bridge.volt
new file mode 100644
index 000000000..c337fbcd5
--- /dev/null
+++ b/src/opnsense/mvc/app/views/OPNsense/Interface/bridge.volt
@@ -0,0 +1,45 @@
+{#
+ # Copyright (c) 2025 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.
+ #}
+
+
+
+ {{ partial('layout_partials/base_bootgrid_table', formGridBridge)}}
+
+{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/interfaces/bridge_settings/reconfigure'}) }}
+{{ partial('layout_partials/base_dialog',['fields':formDialogBridge,'id':formGridBridge['edit_dialog_id'],'label':lang._('Edit Bridge')])}}
diff --git a/src/opnsense/scripts/interfaces/reconfigure_bridges.php b/src/opnsense/scripts/interfaces/reconfigure_bridges.php
new file mode 100755
index 000000000..a0a9d5b67
--- /dev/null
+++ b/src/opnsense/scripts/interfaces/reconfigure_bridges.php
@@ -0,0 +1,53 @@
+#!/usr/local/bin/php
+
-
-
-
-
-
-
-
- 0) print_input_errors($input_errors); ?>
-
-
-
-
-
- false]) as $intf => $intfdata) {
- if (substr($intfdata['if'], 0, 3) != 'gre' && substr($intfdata['if'], 0, 2) != 'lo') {
- $ifacelist[$intf] = $intfdata['descr'];
- }
-}
-
-if ($_SERVER['REQUEST_METHOD'] === 'GET') {
- // read form data
- if (isset($_GET['id']) && !empty($a_bridges[$_GET['id']])) {
- $id = $_GET['id'];
- }
- // copy fields 1-on-1
- $copy_fields = ['descr', 'bridgeif', 'maxaddr', 'timeout', 'maxage','fwdelay', 'proto', 'holdcnt', 'span'];
- foreach ($copy_fields as $fieldname) {
- if (isset($a_bridges[$id][$fieldname])) {
- $pconfig[$fieldname] = $a_bridges[$id][$fieldname];
- } else {
- $pconfig[$fieldname] = null;
- }
- }
-
- // bool fields
- $pconfig['enablestp'] = !empty($a_bridges[$id]['enablestp']);
- $pconfig['linklocal'] = !empty($a_bridges[$id]['linklocal']);
-
- // simple array fields
- $array_fields = ['members', 'stp', 'edge', 'autoedge', 'ptp', 'autoptp', 'static', 'private'];
- foreach ($array_fields as $fieldname) {
- if (!empty($a_bridges[$id][$fieldname])) {
- $pconfig[$fieldname] = explode(',', $a_bridges[$id][$fieldname]);
- } else {
- $pconfig[$fieldname] = [];
- }
- }
-} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
- // save / validate formdata
- if (isset($_POST['id']) && !empty($a_bridges[$_POST['id']])) {
- $id = $_POST['id'];
- }
-
- $input_errors = [];
- $pconfig = $_POST;
-
- $not_between_int = function($val, $min, $max) {
- return !is_numeric($val) || $val < $min || $val > $max;
- };
-
- if (!empty($pconfig['maxage']) && $not_between_int($pconfig['maxage'], 6, 40)) {
- $input_errors[] = gettext("Maxage needs to be an integer between 6 and 40.");
- }
- if ($pconfig['maxaddr'] != "" && $not_between_int($pconfig['maxaddr'], 0, PHP_INT_MAX)) {
- $input_errors[] = gettext("Maxaddr needs to be an integer.");
- }
- if ($pconfig['timeout'] != "" && $not_between_int($pconfig['timeout'], 0, PHP_INT_MAX)) {
- $input_errors[] = gettext("Timeout needs to be an integer.");
- }
- if (!empty($pconfig['fwdelay']) && $not_between_int($pconfig['fwdelay'], 4, 30)) {
- $input_errors[] = gettext("Forward Delay needs to be an integer between 4 and 30.");
- }
- if (!empty($pconfig['holdcnt']) && $not_between_int($pconfig['holdcnt'], 1, 10)) {
- $input_errors[] = gettext("Transmit Hold Count for STP needs to be an integer between 1 and 10.");
- }
-
- $members = !empty($pconfig['members']) ? $pconfig['members'] : [];
- if (!empty($members)) {
- foreach($members as $ifmembers) {
- if (empty($config['interfaces'][$ifmembers])) {
- $input_errors[] = gettext("A member interface passed does not exist in configuration");
- }
- if (!empty($config['interfaces'][$ifmembers]['wireless']['mode']) && $config['interfaces'][$ifmembers]['wireless']['mode'] != "hostap") {
- $input_errors[] = gettext("Bridging a wireless interface is only possible in hostap mode.");
- }
- if ($pconfig['span'] != "none" && $pconfig['span'] == $ifmembers) {
- $input_errors[] = gettext("Span interface cannot be part of the bridge. Remove the span interface from bridge members to continue.");
- }
- }
- }
-
- foreach (['stp', 'edge', 'autoedge', 'ptp', 'autoptp', 'static', 'private'] as $section) {
- if (!empty($pconfig[$section])) {
- foreach ($pconfig[$section] as $if) {
- if (!in_array($if, $members)) {
- $ifname = !empty($ifacelist[$if]) ? $ifacelist[$if] : $if;
- $input_errors[] = gettext(sprintf("Option %s contains non bridge member interface %s", $section, $ifname));
- }
- }
- }
- }
-
- if (count($input_errors) == 0) {
- $bridge = [];
-
- // booleans
- foreach (['enablestp', 'linklocal'] as $fieldname) {
- if (!empty($pconfig[$fieldname])) {
- $bridge[$fieldname] = true;
- }
- }
-
- // 1 on 1 copy
- $copy_fields = ['descr', 'maxaddr', 'timeout', 'bridgeif', 'maxage','fwdelay', 'proto', 'holdcnt'];
- foreach ($copy_fields as $fieldname) {
- if (isset($pconfig[$fieldname]) && $pconfig[$fieldname] != '') {
- $bridge[$fieldname] = $pconfig[$fieldname];
- } else {
- $bridge[$fieldname] = null;
- }
- }
- if ($pconfig['span'] != 'none') {
- $bridge['span'] = $pconfig['span'];
- }
-
- // simple array fields
- $array_fields = ['members', 'stp', 'edge', 'autoedge', 'ptp', 'autoptp', 'static', 'private'];
- foreach ($array_fields as $fieldname) {
- if(!empty($pconfig[$fieldname])) {
- $bridge[$fieldname] = implode(',', $pconfig[$fieldname]);
- }
- }
-
- if (empty($bridge['bridgeif'])) {
- $bridge['bridgeif'] = legacy_interface_create('bridge'); /* XXX find another strategy */
- }
-
- if (empty($bridge['bridgeif']) || strpos($bridge['bridgeif'], 'bridge') !== 0) {
- $input_errors[] = gettext("Error occurred creating interface, please retry.");
- } else {
- if (isset($id)) {
- $a_bridges[$id] = $bridge;
- } else {
- $a_bridges[] = $bridge;
- }
- write_config();
- interfaces_bridge_configure($bridge['bridgeif']);
- ifgroup_setup();
- interfaces_restart_by_device(false, [$bridge['bridgeif']]);
- header(url_safe('Location: /interfaces_bridge.php'));
- exit;
- }
- }
-}
-
-legacy_html_escape_form_data($pconfig);
-include("head.inc");
-?>
-
-
-
-
-
-
-
-
- 0) print_input_errors($input_errors); ?>
-
-
-
-
-
-