From c2de70ada23a36668642c66bc82de37c5e80f008 Mon Sep 17 00:00:00 2001 From: Mohd Husaam Mehdi Date: Thu, 5 Mar 2026 20:48:37 +0530 Subject: [PATCH] bridgemngr: add support for bridge config in bridging UCI --- bridgemngr/Makefile | 2 + bridgemngr/files/etc/init.d/bridging | 582 +++++++++++++++++- .../files/etc/uci-defaults/65-update_bridging | 488 +++++++++++++++ 3 files changed, 1050 insertions(+), 22 deletions(-) create mode 100644 bridgemngr/files/etc/uci-defaults/65-update_bridging diff --git a/bridgemngr/Makefile b/bridgemngr/Makefile index f57ead8d5..bec725288 100644 --- a/bridgemngr/Makefile +++ b/bridgemngr/Makefile @@ -68,6 +68,7 @@ endif define Package/bridgemngr/install $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DIR) $(1)/etc/uci-defaults ifeq ($(CONFIG_BRIDGEMNGR_USE_DM_FRAMEWORK),y) $(call Build/Install/DM,$(PKG_BUILD_DIR)/dmf,$(PKG_BUILD_DIR)/dmf,$(1),bridgemngr) @@ -82,6 +83,7 @@ endif $(INSTALL_BIN) ./files/etc/init.d/bridging $(1)/etc/init.d/ $(INSTALL_DATA) ./files/etc/config/bridging $(1)/etc/config/ + $(INSTALL_DATA) ./files/etc/uci-defaults/65-update_bridging $(1)/etc/uci-defaults/ endef ifeq ($(LOCAL_DEV),1) diff --git a/bridgemngr/files/etc/init.d/bridging b/bridgemngr/files/etc/init.d/bridging index 37bb2cd29..d88c10a74 100755 --- a/bridgemngr/files/etc/init.d/bridging +++ b/bridgemngr/files/etc/init.d/bridging @@ -1,22 +1,169 @@ #!/bin/sh /etc/rc.common # Start after bdmf shell, wanconf, and switch-script but before the network-script -START=20 +START=19 STOP=10 USE_PROCD=1 +BRIDGING_CONFIG="/etc/config/bridging" +NETWORK_CONFIG="/etc/config/network" + +# TODO: +# Currently all the bridges are wiped out and written in network UCI +# We might want to optimize it and only write the delta in the network UCI + +# TODO: +# propagate vlanfiltering option from bridging to network uci, in case backend is legacy + . /lib/functions.sh +has_interface_for_device() { + local dev="$1" + local ifsec + + for ifsec in $(uci -q show network | awk -F'[.=]' '/^network\..*=interface/ {print $2}'); do + [ "$(uci -q get network.$ifsec.device 2>/dev/null)" = "$dev" ] && return 0 + done + + return 1 +} + +# Return 0 if a section name looks like a preserved ethernet base port +# (LAN1, LAN2, ..., LANn, WAN — case-insensitive) +is_preserved_section() { + local name="$1" + echo "$name" | grep -qiE '^(lan[0-9]+|wan)$' +} + +# Remove all device and interface sections from /etc/config/network that were +# created by a previous run of this script, while preserving base-port sections. +cleanup_network() { + config_load network + + # First, find all bridge devices and delete interfaces that use them + config_foreach _cleanup_bridge_interfaces device + + # Then delete all VLAN devices (8021ad/8021q) that were created by this script + config_foreach _cleanup_vlan_devices device + + # Also remove any anonymous bridge-vlan sections left from a prior run + local idx=0 + while uci -q get "network.@bridge-vlan[$idx]" >/dev/null 2>&1; do + uci -q delete "network.@bridge-vlan[$idx]" + done +} + +# Delete interfaces that reference a bridge device +_cleanup_bridge_interfaces() { + local cfg="$1" + local type devname + + type=$(uci -q get "network.$cfg.type" 2>/dev/null) + devname=$(uci -q get "network.$cfg.name" 2>/dev/null) + + [ "$type" = "bridge" ] || return + + # Find and delete all interfaces that use this bridge + config_foreach _check_delete_interface_for_bridge interface "$devname" + + # Delete the bridge device section itself + uci -q delete "network.$cfg" +} + +# Check if an interface uses a specific bridge, delete if it does +_check_delete_interface_for_bridge() { + local cfg="$1" + local bridge_name="$2" + local ifdevice managed_by_bridging + + managed_by_bridging=$(uci -q get "network.$cfg.managed_by_bridging" 2>/dev/null) + [ "$managed_by_bridging" = "1" ] || return + + ifdevice=$(uci -q get "network.$cfg.device" 2>/dev/null) + + # Delete if device matches the bridge or is a sub-interface of it + if [ "$ifdevice" = "$bridge_name" ] || echo "$ifdevice" | grep -q "^${bridge_name}\."; then + uci -q delete "network.$cfg" + fi +} + +# Delete VLAN devices (8021ad/8021q) created by this script +_cleanup_vlan_devices() { + local cfg="$1" + local type managed + + type=$(uci -q get "network.$cfg.type" 2>/dev/null) + managed=$(uci -q get "network.$cfg.managed_by_bridging" 2>/dev/null) + + if [ "$managed" = "1" ] && { [ "$type" = "8021ad" ] || [ "$type" = "8021q" ]; }; then + uci -q delete "network.$cfg" + fi +} + +# --------------------------------------------------------------------------- +# Determine 8021q / 8021ad type from tpid value (decimal or hex string) +# Sets global _vlan_type +# --------------------------------------------------------------------------- +vlan_type_from_tpid() { + local tpid="$1" + case "$tpid" in + 88a8|0x88a8|34984) _vlan_type="8021ad" ;; + 8100|0x8100|33024) _vlan_type="8021q" ;; + *) _vlan_type="8021q" ;; + esac +} + +# --------------------------------------------------------------------------- +# Ensure an anonymous VLAN device section exists; create if absent. +# Usage: ensure_vlan_device +# Sets _ensured_devname to the section name used (same as ). +# --------------------------------------------------------------------------- +ensure_vlan_device() { + local devname="$1" devtype="$2" vid="$3" ifname="$4" + + # Check if already added in this run (track by interface name) + echo "$_created_devices" | grep -qF "|${devname}|" && return + + uci -q add network device + uci -q set "network.@device[-1].name=${devname}" + uci -q set "network.@device[-1].type=${devtype}" + uci -q set "network.@device[-1].vid=${vid}" + uci -q set "network.@device[-1].ifname=${ifname}" + uci -q set "network.@device[-1].enabled=1" + uci -q set "network.@device[-1].managed_by_bridging=1" + + _created_devices="${_created_devices}|${devname}|" +} + +# --------------------------------------------------------------------------- +# Iface counter — generates iface1, iface2, ... +# --------------------------------------------------------------------------- +_iface_counter=0 +next_iface_name() { + _iface_counter=$(( _iface_counter + 1 )) + _iface_name="iface${_iface_counter}" +} + +# --------------------------------------------------------------------------- +# Add a port to a space-separated list, avoiding duplicates. +# Usage: _list_add +# --------------------------------------------------------------------------- +_list_add() { + local _varname="$1" + local _val="$2" + eval "local _cur=\"\${${_varname}}\"" + echo " ${_cur} " | grep -qF " ${_val} " && return + eval "${_varname}=\"\${_cur} \${_val}\"" +} + +# =========================================================================== +# ebtables helpers (unchanged) +# =========================================================================== + handle_ebtables_chain() { local sid="$1" - local table - local chain - local target - local policy - local append - local enabled - local ret + local table chain target policy append enabled ret config_get table "$sid" table filter config_get chain "$sid" chain @@ -34,7 +181,7 @@ handle_ebtables_chain() { append="-I" fi - ebtables --concurrent -t "$table" -N "$target" -P "$policy" 2> /dev/null + ebtables --concurrent -t "$table" -N "$target" -P "$policy" 2>/dev/null ret=$? if [ $ret -eq 0 ]; then @@ -47,13 +194,7 @@ handle_ebtables_chain() { handle_ebtables_rule() { local sid="$1" - local table - local chain - local target - local match - local value - local enabled - local ret + local table chain target match value append enabled config_get table "$sid" table filter config_get chain "$sid" chain @@ -72,22 +213,419 @@ handle_ebtables_rule() { append="-I" fi - ebtables --concurrent -t "$table" -D "$chain" ${match} -j "$target" ${value} 2> /dev/null + ebtables --concurrent -t "$table" -D "$chain" ${match} -j "$target" ${value} 2>/dev/null ebtables --concurrent -t "$table" ${append} "$chain" ${match} -j "$target" ${value} } -start_service() { +# =========================================================================== +# bridge-vlan backend +# =========================================================================== + +# Per-bridge globals (reset per bridge): +# _bv_bridge_ports — ports list for the bridge device section +# _bv_count — number of bridge-vlan sections found + +convert_bridge_vlan() { + local bridge_section="$1" + local bridge_name + local bridge_enabled bridge_stp bridge_priority bridge_hello_time bridge_max_age bridge_forward_delay + + config_get_bool bridge_enabled "$bridge_section" enabled 1 + config_get bridge_name "$bridge_section" name + config_get bridge_stp "$bridge_section" stp + config_get bridge_priority "$bridge_section" priority + config_get bridge_hello_time "$bridge_section" hello_time + config_get bridge_max_age "$bridge_section" max_age + config_get bridge_forward_delay "$bridge_section" forward_delay + + _bv_bridge_ports="" + _bv_count=0 + + # Collect all bridge-ports for this bridge (the logical member ports) + config_foreach _collect_bridge_port bridge-port "$bridge_section" + + # Walk bridge-vlan sections; for each, walk their vlan-ports to build + # network bridge-vlan sections and any required intermediate devices. + config_foreach _bv_handle_bridge_vlan bridge-vlan "$bridge_section" "$bridge_name" + + # ------------------------------------------------------------------ + # Create the bridge device section + # ------------------------------------------------------------------ + uci -q set "network.${bridge_section}=device" + uci -q set "network.${bridge_section}.name=${bridge_name}" + uci -q set "network.${bridge_section}.type=bridge" + uci -q set "network.${bridge_section}.bridge_empty=1" + uci -q set "network.${bridge_section}.enabled=${bridge_enabled}" + [ -n "$bridge_stp" ] && uci -q set "network.${bridge_section}.stp=${bridge_stp}" + [ -n "$bridge_priority" ] && uci -q set "network.${bridge_section}.priority=${bridge_priority}" + [ -n "$bridge_hello_time" ] && uci -q set "network.${bridge_section}.hello_time=${bridge_hello_time}" + [ -n "$bridge_max_age" ] && uci -q set "network.${bridge_section}.max_age=${bridge_max_age}" + [ -n "$bridge_forward_delay" ] && uci -q set "network.${bridge_section}.forward_delay=${bridge_forward_delay}" + uci -q set "network.${bridge_section}.managed_by_bridging=1" + + for p in $_bv_bridge_ports; do + uci -q add_list "network.${bridge_section}.ports=${p}" + done + + # ------------------------------------------------------------------ + # Interface management + # ------------------------------------------------------------------ + if [ "$_bv_count" -eq 0 ]; then + # No VLANs — create a plain interface for the bridge + if ! has_interface_for_device "${bridge_name}"; then + next_iface_name + uci -q set "network.${_iface_name}=interface" + uci -q set "network.${_iface_name}.proto=none" + uci -q set "network.${_iface_name}.disabled=0" + uci -q set "network.${_iface_name}.device=${bridge_name}" + uci -q set "network.${_iface_name}.managed_by_bridging=1" + fi + else + # VLANs present — remove any stale plain interface for the bridge itself + local ifsec + for ifsec in $(uci -q show network | awk -F'[.=]' '/^network\..*=interface/ {print $2}'); do + local ifdevice managed_by_bridging + ifdevice=$(uci -q get "network.$ifsec.device" 2>/dev/null) + managed_by_bridging=$(uci -q get "network.$ifsec.managed_by_bridging" 2>/dev/null) + [ "$ifdevice" = "$bridge_name" ] && [ "$managed_by_bridging" = "1" ] && \ + uci -q delete "network.$ifsec" + done + fi +} + +# Collect enabled bridge-port members into _bv_bridge_ports +_collect_bridge_port() { + local cfg="$1" + local want_bridge="$2" + + local bsec portname enabled management + config_get bsec "$cfg" bridge + config_get portname "$cfg" name + config_get_bool enabled "$cfg" enabled 1 + config_get_bool management "$cfg" management 0 + + [ "$enabled" = "1" ] || return + [ "$management" = "0" ] || return + [ "$bsec" = "$want_bridge" ] || return + + _list_add _bv_bridge_ports "$portname" +} + +# Handle one bridge-vlan section (bridge-vlan backend) +_bv_handle_bridge_vlan() { + local cfg="$1" + local want_bridge="$2" + local bridge_name="$3" + + local bsec vlan enabled + config_get bsec "$cfg" bridge + config_get vlan "$cfg" vlan + config_get_bool enabled "$cfg" enabled 1 + + [ "$bsec" = "$want_bridge" ] || return + [ "$enabled" = "1" ] || return + + _bv_count=$(( _bv_count + 1 )) + + # Create the network bridge-vlan section + uci -q add network bridge-vlan + uci -q set "network.@bridge-vlan[-1].device=${bridge_name}" + uci -q set "network.@bridge-vlan[-1].vlan=${vlan}" + + # Walk vlan-port sections that belong to this bridge-vlan + config_foreach _bv_handle_vlan_port vlan-port "$want_bridge" "$cfg" "$bridge_name" "$vlan" + + # Create an interface for the VLAN sub-interface of the bridge + if ! has_interface_for_device "${bridge_name}.${vlan}"; then + next_iface_name + uci -q set "network.${_iface_name}=interface" + uci -q set "network.${_iface_name}.proto=none" + uci -q set "network.${_iface_name}.disabled=0" + uci -q set "network.${_iface_name}.device=${bridge_name}.${vlan}" + uci -q set "network.${_iface_name}.managed_by_bridging=1" + fi +} + +# Handle one vlan-port section for the bridge-vlan backend. +# +# Bridging options: +# port — base interface name (e.g. eth0, ae_wan) +# vlan — reference to a bridge-vlan section name +# untagged — 0=tagged, 1=untagged +# pvid — 1=this is the PVID for the port (adds * in annotation) +# tpid — outer tag protocol (decimal or hex); present → svid stacking +# svid — outer VLAN id for stacking +# +# Scenarios: +# A) untagged=0, no svid → port:t +# B) untagged=1, no svid → port:u[*] +# C) untagged=0, svid set → create s-vlan dev; sdev:t +# D) untagged=1, pvid=1, svid set, +# tpid matches vlan (svid==vlan) → create s-vlan dev; sdev:u* +# E) untagged=1, pvid=1, svid set, +# tpid != vlan (svid!=vlan) → create s-vlan dev; sdev:u* +# (covers scenarios 1 and 3 from the spec) +# F) untagged=0, pvid=1, svid set → create s-vlan dev; sdev:t +# (scenario 4 — double tag, c-vlan still goes in as tagged) +# +# Also adds both the raw port AND the s-vlan device to _bv_bridge_ports +# when an s-vlan device is created. +_bv_handle_vlan_port() { + local cfg="$1" + local want_bridge="$2" + local want_bv_cfg="$3" # bridge-vlan section name (e.g. bv1) + local bridge_name="$4" + local vlan="$5" + + local bsec bvcfg portname untagged pvid tpid svid enabled + config_get_bool enabled "$cfg" enabled 1 + config_get bsec "$cfg" bridge + config_get bvcfg "$cfg" vlan + config_get portname "$cfg" port + config_get_bool untagged "$cfg" untagged 0 + config_get_bool pvid "$cfg" pvid 0 + config_get tpid "$cfg" tpid "" + config_get svid "$cfg" svid "" + + [ "$enabled" = "1" ] || return + [ "$bsec" = "$want_bridge" ] || return + [ "$bvcfg" = "$want_bv_cfg" ] || return + + if [ -n "$svid" ] && [ -n "$tpid" ]; then + # There is an outer tag — create an intermediate device + vlan_type_from_tpid "$tpid" + local outer_devname="${portname}.${svid}" + + ensure_vlan_device "$outer_devname" "$_vlan_type" "$svid" "$portname" + + # The raw port must also be in the bridge's ports list + _list_add _bv_bridge_ports "$portname" + # And the s-vlan device itself + _list_add _bv_bridge_ports "$outer_devname" + + # Annotation for the bridge-vlan ports entry + local annotation + if [ "$untagged" = "1" ] && [ "$pvid" = "1" ]; then + annotation="u*" + elif [ "$untagged" = "1" ]; then + annotation="u" + else + annotation="t" + fi + + uci -q add_list "network.@bridge-vlan[-1].ports=${outer_devname}:${annotation}" + + else + # No outer tag — plain port + # pvid only makes sense with untagged; pvid adds '*' suffix + local annotation + if [ "$untagged" = "1" ] && [ "$pvid" = "1" ]; then + annotation="u*" + elif [ "$untagged" = "1" ]; then + annotation="u" + else + annotation="t" + fi + + uci -q add_list "network.@bridge-vlan[-1].ports=${portname}:${annotation}" + fi +} + +# =========================================================================== +# Legacy backend +# =========================================================================== + +# Per-bridge globals (reset per bridge): +# _legacy_bridge_ports — ports list for the bridge device section + +convert_legacy() { + local bridge_section="$1" + local bridge_name + local bridge_enabled bridge_stp bridge_priority bridge_hello_time bridge_max_age bridge_forward_delay + + config_get_bool bridge_enabled "$bridge_section" enabled 1 + config_get bridge_name "$bridge_section" name + config_get bridge_stp "$bridge_section" stp + config_get bridge_priority "$bridge_section" priority + config_get bridge_hello_time "$bridge_section" hello_time + config_get bridge_max_age "$bridge_section" max_age + config_get bridge_forward_delay "$bridge_section" forward_delay + + logger -t bridging -p daemon.warn \ + "Legacy bridge backend: all VLANs share the same bridge '${bridge_name}'. Consider creating separate bridges per VLAN." + + _legacy_bridge_ports="" + + # Walk bridge-vlan sections; for each, walk their vlan-ports + config_foreach _legacy_handle_bridge_vlan bridge-vlan "$bridge_section" "$bridge_name" + + # Create the bridge device + uci -q set "network.${bridge_section}=device" + uci -q set "network.${bridge_section}.name=${bridge_name}" + uci -q set "network.${bridge_section}.type=bridge" + uci -q set "network.${bridge_section}.bridge_empty=1" + uci -q set "network.${bridge_section}.enabled=${bridge_enabled}" + [ -n "$bridge_stp" ] && uci -q set "network.${bridge_section}.stp=${bridge_stp}" + [ -n "$bridge_priority" ] && uci -q set "network.${bridge_section}.priority=${bridge_priority}" + [ -n "$bridge_hello_time" ] && uci -q set "network.${bridge_section}.hello_time=${bridge_hello_time}" + [ -n "$bridge_max_age" ] && uci -q set "network.${bridge_section}.max_age=${bridge_max_age}" + [ -n "$bridge_forward_delay" ] && uci -q set "network.${bridge_section}.forward_delay=${bridge_forward_delay}" + uci -q set "network.${bridge_section}.managed_by_bridging=1" + + for p in $_legacy_bridge_ports; do + uci -q add_list "network.${bridge_section}.ports=${p}" + done + + # Single interface for the whole bridge + if ! has_interface_for_device "${bridge_name}"; then + next_iface_name + uci -q set "network.${_iface_name}=interface" + uci -q set "network.${_iface_name}.proto=none" + uci -q set "network.${_iface_name}.disabled=0" + uci -q set "network.${_iface_name}.device=${bridge_name}" + uci -q set "network.${_iface_name}.managed_by_bridging=1" + fi +} + +# Handle one bridge-vlan section (legacy backend) +_legacy_handle_bridge_vlan() { + local cfg="$1" + local want_bridge="$2" + local bridge_name="$3" + + local bsec vlan enabled + config_get_bool enabled "$cfg" enabled 1 + config_get bsec "$cfg" bridge + config_get vlan "$cfg" vlan + + [ "$enabled" = "1" ] || return + [ "$bsec" = "$want_bridge" ] || return + + # Walk vlan-port sections that belong to this bridge-vlan + config_foreach _legacy_handle_vlan_port vlan-port "$want_bridge" "$cfg" "$vlan" +} + +# Handle one vlan-port section for the legacy backend. +# +# In legacy mode the bridge has no VLAN awareness — we create actual +# VLAN sub-interface devices and add those as physical ports of the bridge. +# +# Decision tree for port device name added to the bridge: +# +# Case 1: no svid, untagged=1 → use portname directly +# Case 2: no svid, untagged=0 → create portname.vlan (8021q), add portname.vlan +# Case 3: svid set, untagged=1, pvid=1 +# and svid == vlan (tpid is 8021ad or 8021q matching the bridge vlan) +# → frames leave with ONE tag (the s-vlan). +# create outer device (portname.svid), add portname.svid +# Case 4: svid set, untagged=1, pvid=1 +# svid != vlan +# → frames leave with ONE tag (svid replaces the c-vlan). +# create outer device (portname.svid), add portname.svid +# (scenarios 1 and 3 collapse here for legacy) +# Case 5: svid set, untagged=0 +# → frames get both outer and inner tag. +# create outer device (portname.svid), +# create inner device (portname.svid.vlan), +# add portname.svid.vlan +# (scenario 4 for legacy) +_legacy_handle_vlan_port() { + local cfg="$1" + local want_bridge="$2" + local want_bv_cfg="$3" + local vlan="$4" + + local bsec bvcfg portname untagged pvid tpid svid enabled + config_get_bool enabled "$cfg" enabled 1 + config_get bsec "$cfg" bridge + config_get bvcfg "$cfg" vlan + config_get portname "$cfg" port + config_get_bool untagged "$cfg" untagged 0 + config_get_bool pvid "$cfg" pvid 0 + config_get tpid "$cfg" tpid "" + config_get svid "$cfg" svid "" + + [ "$enabled" = "1" ] || return + [ "$bsec" = "$want_bridge" ] || return + [ "$bvcfg" = "$want_bv_cfg" ] || return + + if [ -n "$svid" ] && [ -n "$tpid" ]; then + vlan_type_from_tpid "$tpid" + local outer_devname="${portname}.${svid}" + + ensure_vlan_device "$outer_devname" "$_vlan_type" "$svid" "$portname" + + if [ "$untagged" = "1" ] && [ "$pvid" = "1" ]; then + # Frames leave with just the outer tag — bridge port is the outer device + _list_add _legacy_bridge_ports "$outer_devname" + else + # untagged=0 with svid — frames get outer AND inner (c-vlan) tag + local inner_devname="${outer_devname}.${vlan}" + ensure_vlan_device "$inner_devname" "8021q" "$vlan" "$outer_devname" + _list_add _legacy_bridge_ports "$inner_devname" + fi + + elif [ "$untagged" = "1" ]; then + # No outer tag, untagged — use raw port + _list_add _legacy_bridge_ports "$portname" + + else + # No outer tag, tagged — create a c-vlan device + local sub_devname="${portname}.${vlan}" + ensure_vlan_device "$sub_devname" "8021q" "$vlan" "$portname" + _list_add _legacy_bridge_ports "$sub_devname" + fi +} + +# =========================================================================== +# Top-level apply +# =========================================================================== + +apply_config() { + if [ ! -f "$BRIDGING_CONFIG" ]; then + return 0 + fi + config_load bridging + config_foreach handle_ebtables_chain chain - config_foreach handle_ebtables_rule rule + config_foreach handle_ebtables_rule rule + + local backend + config_get backend global bridge_backend "bridge-vlan" + + case "$backend" in + bridge-vlan) + _created_devices="" + cleanup_network + config_load bridging + config_foreach convert_bridge_vlan bridge + uci -q commit network + /etc/init.d/network reload + ;; + legacy) + _created_devices="" + cleanup_network + config_load bridging + config_foreach convert_legacy bridge + uci -q commit network + /etc/init.d/network reload + ;; + *) + logger -t bridging -p daemon.err "Unknown bridge_backend '$backend', aborting." + return 1 + ;; + esac +} + +start_service() { + apply_config } reload_service() { - stop - start + apply_config } - service_triggers() { procd_add_reload_trigger bridging } diff --git a/bridgemngr/files/etc/uci-defaults/65-update_bridging b/bridgemngr/files/etc/uci-defaults/65-update_bridging new file mode 100644 index 000000000..7f781826c --- /dev/null +++ b/bridgemngr/files/etc/uci-defaults/65-update_bridging @@ -0,0 +1,488 @@ +#!/bin/sh +. /lib/functions.sh + +# --------------------------------------------------------------------------- +# Read network UCI bridges and sync to bridging config +# --------------------------------------------------------------------------- +# Reads bridge devices from /etc/config/network and creates corresponding +# sections in /etc/config/bridging: +# +# - bridge — one per bridge device +# - bridge-port — one per physical member port of the bridge +# - bridge-vlan — one per network bridge-vlan section (bridge-vlan backend) +# OR inferred from device chain (legacy backend) +# - vlan-port — one per port entry within a bridge-vlan +# +# Sections already written by the bridging init script (managed_by_bridging=1) +# are skipped so we never import our own output back. +# +# The global config section 'global' is NOT modified; the caller is responsible +# for setting bridge_backend before or after calling this script. +# --------------------------------------------------------------------------- + +# Counters for generated anonymous-style section names +_bp_counter=0 +_bv_counter=0 +_vp_counter=0 + +_next_bp() { _bp_counter=$(( _bp_counter + 1 )); _bp_name="bp${_bp_counter}"; } +_next_bv() { _bv_counter=$(( _bv_counter + 1 )); _bv_name="bv${_bv_counter}"; } +_next_vp() { _vp_counter=$(( _vp_counter + 1 )); _vp_name="vp${_vp_counter}"; } + +# --------------------------------------------------------------------------- +# Device map — built once from all non-managed, non-bridge device sections. +# +# Two flat string maps, keyed by device name: +# _devmap_type — "|name=type|name=type|..." +# _devmap_ifname — "|name=ifname|name=ifname|..." +# +# Lookup helpers set globals _dm_type / _dm_ifname; return 1 if not found. +# --------------------------------------------------------------------------- +_devmap_type="" +_devmap_ifname="" + +_build_device_map() { + local sec sectype secname secifname managed + + for sec in $(uci -q show network | awk -F'[.=]' '/^network\..*=device$/ {print $2}'); do + managed=$(uci -q get "network.$sec.managed_by_bridging" 2>/dev/null) + [ "$managed" = "1" ] && continue + + sectype=$(uci -q get "network.$sec.type" 2>/dev/null) + secname=$(uci -q get "network.$sec.name" 2>/dev/null) + secifname=$(uci -q get "network.$sec.ifname" 2>/dev/null) + + [ -z "$secname" ] && continue + # Skip bridge devices — they are processed separately + [ "$sectype" = "bridge" ] && continue + + _devmap_type="${_devmap_type}|${secname}=${sectype}|" + [ -n "$secifname" ] && \ + _devmap_ifname="${_devmap_ifname}|${secname}=${secifname}|" + done +} + +# Look up type for a device name. Sets _dm_type; returns 1 if not found. +_devmap_get_type() { + local name="$1" + _dm_type=$(printf '%s' "$_devmap_type" | \ + sed -n "s/.*|${name}=\([^|]*\)|.*/\1/p" | head -1) + [ -n "$_dm_type" ] +} + +# Look up ifname (parent) for a device name. Sets _dm_ifname; returns 1 if not found. +_devmap_get_ifname() { + local name="$1" + _dm_ifname=$(printf '%s' "$_devmap_ifname" | \ + sed -n "s/.*|${name}=\([^|]*\)|.*/\1/p" | head -1) + [ -n "$_dm_ifname" ] +} + +# --------------------------------------------------------------------------- +# Given a device name that appears as a bridge port, walk the device map +# to reconstruct the full VLAN chain back to the physical base interface. +# +# Sets globals (all may be empty if not applicable): +# _chain_base — the physical / base interface name (no VLAN device entry) +# _chain_svid — outer VLAN id (s-vlan), if present +# _chain_tpid — outer tpid decimal (34984=8021ad, 33024=8021q), if svid set +# _chain_vid — inner / only VLAN id, if this port has a VLAN device +# _chain_cvtpid — inner tpid decimal, if double-stacked +# +# Strategy: follow ifname pointers upward until we reach a name that has +# no entry in the device map (i.e. it is a physical interface). Collect +# type + vid at each hop. +# --------------------------------------------------------------------------- +_resolve_port_chain() { + local devname="$1" + + _chain_base="" + _chain_svid="" + _chain_tpid="" + _chain_vid="" + _chain_cvtpid="" + + # Collect hops: each hop is "type:vid:name" from innermost to outermost + # We build the list by walking upward (devname → ifname → ifname → ...) + local hops="" # space-separated, innermost first + local cur="$devname" + + while _devmap_get_type "$cur"; do + local hop_type="$_dm_type" + # Extract vid from the device section (the vid option, not name-based) + local hop_vid + hop_vid=$(_get_device_vid "$cur") + hops="${hops} ${hop_type}:${hop_vid}:${cur}" + + # Walk up to parent + if _devmap_get_ifname "$cur"; then + cur="$_dm_ifname" + else + # No ifname pointer — stop; cur is the base + break + fi + done + + # cur is now the base interface (no device map entry or no ifname) + _chain_base="$cur" + + # Count hops + local nhops=0 + local h + for h in $hops; do nhops=$(( nhops + 1 )); done + + if [ "$nhops" -eq 0 ]; then + # No VLAN device — plain physical port, nothing to set + return + fi + + if [ "$nhops" -eq 1 ]; then + # Single VLAN layer + local h1="$hops" + _chain_vid=$( echo "$h1" | cut -d: -f2) + local t1=$( echo "$h1" | cut -d: -f1) + _chain_tpid=$( _tpid_from_type "$t1") + # Reuse tpid field as the only tag's tpid; no svid + return + fi + + if [ "$nhops" -ge 2 ]; then + # Two (or more) VLAN layers — outermost hop is the s-vlan + # hops are listed innermost-first, so last = outermost + local outer="" inner="" + local idx=0 + for h in $hops; do + idx=$(( idx + 1 )) + [ "$idx" -eq 1 ] && inner="$h" + outer="$h" # last one wins + done + + _chain_svid=$( echo "$outer" | cut -d: -f2) + local ot=$( echo "$outer" | cut -d: -f1) + _chain_tpid=$( _tpid_from_type "$ot") + + _chain_vid=$( echo "$inner" | cut -d: -f2) + local it=$( echo "$inner" | cut -d: -f1) + _chain_cvtpid=$(_tpid_from_type "$it") + fi +} + +# Return decimal tpid for a device type string +_tpid_from_type() { + case "$1" in + 8021ad) echo "34984" ;; + *) echo "33024" ;; + esac +} + +# Return the vid option of a named device section (looks up by name). +_get_device_vid() { + local devname="$1" + local sec + for sec in $(uci -q show network | awk -F'[.=]' '/^network\..*=device$/ {print $2}'); do + local n + n=$(uci -q get "network.$sec.name" 2>/dev/null) + [ "$n" = "$devname" ] || continue + uci -q get "network.$sec.vid" 2>/dev/null + return + done +} + +# --------------------------------------------------------------------------- +# Main entry: read all bridge devices from network UCI +# --------------------------------------------------------------------------- +read_bridges_from_network_uci() { + config_load network + # Build device map once — used by all chain-walking helpers below + _build_device_map + config_foreach _process_network_bridge_device device +} + +# --------------------------------------------------------------------------- +# Process a single device section from network config +# --------------------------------------------------------------------------- +_process_network_bridge_device() { + local cfg="$1" + local devtype devname managed + + devtype=$(uci -q get "network.$cfg.type" 2>/dev/null) + devname=$(uci -q get "network.$cfg.name" 2>/dev/null) + managed=$(uci -q get "network.$cfg.managed_by_bridging" 2>/dev/null) + + # Skip sections we wrote ourselves + [ "$managed" = "1" ] && return + # Only process bridge devices + [ "$devtype" = "bridge" ] || return + + # ------------------------------------------------------------------ + # Create the bridge section in bridging config + # ------------------------------------------------------------------ + uci -q set "bridging.${cfg}=bridge" + uci -q set "bridging.${cfg}.name=${devname}" + uci -q set "bridging.${cfg}.from_network_uci=1" + uci -q set "bridging.${cfg}.enabled=1" + + # add management port section + _next_bp + uci -q set "bridging.${_bp_name}=bridge-port" + uci -q set "bridging.${_bp_name}.bridge=${cfg}" + uci -q set "bridging.${_bp_name}.name=${devname}" + uci -q set "bridging.${_bp_name}.management=1" + uci -q set "bridging.${_bp_name}.from_network_uci=1" + + # Optional bridge parameters + local v + for opt in stp priority hello_time max_age forward_delay; do + v=$(uci -q get "network.$cfg.$opt" 2>/dev/null) + [ -n "$v" ] && uci -q set "bridging.${cfg}.${opt}=${v}" + done + + # ------------------------------------------------------------------ + # Determine which backend to use for this bridge. + # Prefer the global bridging backend if already set; otherwise probe + # for the presence of network bridge-vlan sections that reference + # this bridge device — if found, treat as bridge-vlan backend. + # ------------------------------------------------------------------ + local backend + backend=$(uci -q get "bridging.global.bridge_backend" 2>/dev/null) + if [ -z "$backend" ]; then + # Auto-detect: look for any network bridge-vlan section for this device + if uci -q show network | grep -q "\.device='${devname}'"; then + backend="bridge-vlan" + else + backend="legacy" + fi + fi + + # ------------------------------------------------------------------ + # Read member ports + # ------------------------------------------------------------------ + local raw_ports + raw_ports=$(uci -q get "network.$cfg.ports" 2>/dev/null) + + if [ "$backend" = "bridge-vlan" ]; then + _import_bvbackend "$cfg" "$devname" "$raw_ports" + else + _import_legacy "$cfg" "$devname" "$raw_ports" + fi +} + +# =========================================================================== +# bridge-vlan backend import +# =========================================================================== +# In bridge-vlan mode: +# - Each entry in the bridge's ports list becomes a bridge-port section. +# If the entry is a VLAN sub-interface (e.g. ae_wan.300) we also create +# the intermediate device record (captured via tpid/svid on vlan-port). +# - Each network bridge-vlan section for this bridge becomes a bridge-vlan +# section in bridging config; each ports entry within it becomes a +# vlan-port section. +# =========================================================================== +_import_bvbackend() { + local bridge_cfg="$1" # bridging section name + local bridge_name="$2" # e.g. br-lan + local raw_ports="$3" # space-separated ports from device section + + # -- bridge-port sections for physical members -------------------------- + for portname in $raw_ports; do + _next_bp + uci -q set "bridging.${_bp_name}=bridge-port" + uci -q set "bridging.${_bp_name}.bridge=${bridge_cfg}" + uci -q set "bridging.${_bp_name}.name=${portname}" + uci -q set "bridging.${_bp_name}.enabled=1" + uci -q set "bridging.${_bp_name}.from_network_uci=1" + done + + # -- bridge-vlan + vlan-port sections ----------------------------------- + # Walk all network bridge-vlan sections and find those for this bridge + local idx=0 + while uci -q get "network.@bridge-vlan[$idx]" >/dev/null 2>&1; do + local bv_dev bv_vlan bv_managed + bv_dev=$(uci -q get "network.@bridge-vlan[$idx].device" 2>/dev/null) + bv_vlan=$(uci -q get "network.@bridge-vlan[$idx].vlan" 2>/dev/null) + bv_managed=$(uci -q get "network.@bridge-vlan[$idx].managed_by_bridging" 2>/dev/null) + + # Skip if not for this bridge or written by us + if [ "$bv_dev" = "$bridge_name" ] && [ "$bv_managed" != "1" ]; then + _next_bv + local bv_sec="$_bv_name" + + uci -q set "bridging.${bv_sec}=bridge-vlan" + uci -q set "bridging.${bv_sec}.bridge=${bridge_cfg}" + uci -q set "bridging.${bv_sec}.vlan=${bv_vlan}" + uci -q set "bridging.${bv_sec}.from_network_uci=1" + + # Iterate over the ports list of this bridge-vlan section + local port_list + port_list=$(uci -q get "network.@bridge-vlan[$idx].ports" 2>/dev/null) + local raw_port + for raw_port in $port_list; do + _import_bv_port "$bv_sec" "$bridge_cfg" "$raw_port" + done + fi + + idx=$(( idx + 1 )) + done +} + +# Parse one ports entry from a network bridge-vlan section and create a vlan-port. +# raw_port is e.g.: eth0:t ae_wan.300:u* eth1:u eth0.1.300:t +_import_bv_port() { + local bv_sec="$1" # bridging bridge-vlan section name + local bridge_cfg="$2" # bridging bridge section name + local raw_port="$3" + + # Split annotation from interface name + local portname annotation pvid_flag + portname="${raw_port%%:*}" + local rest="${raw_port#*:}" + [ "$rest" = "$raw_port" ] && rest="" + + pvid_flag=0 + case "$rest" in + u\*) annotation="u"; pvid_flag=1 ;; + u) annotation="u" ;; + t) annotation="t" ;; + *) annotation="t" ;; + esac + + _next_vp + uci -q set "bridging.${_vp_name}=vlan-port" + uci -q set "bridging.${_vp_name}.bridge=${bridge_cfg}" + uci -q set "bridging.${_vp_name}.vlan=${bv_sec}" + uci -q set "bridging.${_vp_name}.from_network_uci=1" + + if [ "$annotation" = "u" ]; then + uci -q set "bridging.${_vp_name}.untagged=1" + else + uci -q set "bridging.${_vp_name}.untagged=0" + fi + [ "$pvid_flag" = "1" ] && uci -q set "bridging.${_vp_name}.pvid=1" + + # Walk the device chain to find base port and any s-vlan wrapping. + # If portname has no device map entry it is already a plain physical port. + _resolve_port_chain "$portname" + # _chain_base, _chain_svid, _chain_tpid are now set + + uci -q set "bridging.${_vp_name}.port=${_chain_base}" + + if [ -n "$_chain_svid" ]; then + uci -q set "bridging.${_vp_name}.svid=${_chain_svid}" + uci -q set "bridging.${_vp_name}.tpid=${_chain_tpid}" + fi +} + +# =========================================================================== +# Legacy backend import +# =========================================================================== +# In legacy mode there are no network bridge-vlan sections. The bridge ports +# list contains VLAN sub-interface device names whose structure is recorded in +# device sections (type, vid, ifname). We walk the device map chain for each +# port to reconstruct bridge-vlan and vlan-port sections. +# +# Chain possibilities (innermost device is the bridge port entry): +# no device entry — plain untagged physical port +# one hop (type, vid) — single-tagged port; vid is the bridge-vlan vid +# two hops (outer, inner) — double-tagged; outer=s-vlan, inner=c-vlan +# =========================================================================== +_import_legacy() { + local bridge_cfg="$1" + local bridge_name="$2" + local raw_ports="$3" + + _legacy_bv_seen="" + _legacy_bp_seen="" + + for portname in $raw_ports; do + _import_legacy_port "$bridge_cfg" "$portname" + done +} + +_import_legacy_port() { + local bridge_cfg="$1" + local portname="$2" + + # Walk the device chain for this port entry + _resolve_port_chain "$portname" + # Globals: _chain_base, _chain_svid, _chain_tpid, _chain_vid, _chain_cvtpid + + if [ -z "$_chain_vid" ]; then + # No VLAN device at all — plain untagged port + _ensure_legacy_bp "$bridge_cfg" "$_chain_base" + return + fi + + # Ensure bridge-port for the physical base + _ensure_legacy_bp "$bridge_cfg" "$_chain_base" + + # Ensure bridge-vlan for the inner (c-vlan) vid + local bv_sec + _ensure_legacy_bv "$bridge_cfg" "$_chain_vid" + bv_sec="$_ensured_bv" + + _next_vp + uci -q set "bridging.${_vp_name}=vlan-port" + uci -q set "bridging.${_vp_name}.bridge=${bridge_cfg}" + uci -q set "bridging.${_vp_name}.vlan=${bv_sec}" + uci -q set "bridging.${_vp_name}.port=${_chain_base}" + uci -q set "bridging.${_vp_name}.untagged=0" + uci -q set "bridging.${_vp_name}.from_network_uci=1" + + if [ -n "$_chain_svid" ]; then + # Double-tagged: record outer s-vlan + uci -q set "bridging.${_vp_name}.svid=${_chain_svid}" + uci -q set "bridging.${_vp_name}.tpid=${_chain_tpid}" + else + # Single-tagged: tpid comes from the single hop's type + uci -q set "bridging.${_vp_name}.tpid=${_chain_tpid}" + fi +} + +# Ensure a bridge-port section exists for a given base interface. +_ensure_legacy_bp() { + local bridge_cfg="$1" + local base="$2" + + echo "$_legacy_bp_seen" | grep -qF "|${bridge_cfg}:${base}|" && return + + _next_bp + uci -q set "bridging.${_bp_name}=bridge-port" + uci -q set "bridging.${_bp_name}.bridge=${bridge_cfg}" + uci -q set "bridging.${_bp_name}.name=${base}" + uci -q set "bridging.${_bp_name}.enabled=1" + uci -q set "bridging.${_bp_name}.from_network_uci=1" + + _legacy_bp_seen="${_legacy_bp_seen}|${bridge_cfg}:${base}|" +} + +# Ensure a bridge-vlan section exists for (bridge_cfg, vid). +# Sets _ensured_bv to the section name. +_ensure_legacy_bv() { + local bridge_cfg="$1" + local vid="$2" + + # Check if already created + local key="${bridge_cfg}:${vid}" + if echo "$_legacy_bv_seen" | grep -qF "|${key}="; then + _ensured_bv=$(echo "$_legacy_bv_seen" | \ + sed -n "s/.*|\(${key}=\([^|]*\)\)|.*/\2/p") + return + fi + + _next_bv + uci -q set "bridging.${_bv_name}=bridge-vlan" + uci -q set "bridging.${_bv_name}.bridge=${bridge_cfg}" + uci -q set "bridging.${_bv_name}.vlan=${vid}" + uci -q set "bridging.${_bv_name}.from_network_uci=1" + + _legacy_bv_seen="${_legacy_bv_seen}|${key}=${_bv_name}|" + _ensured_bv="$_bv_name" +} + +# --------------------------------------------------------------------------- +read_bridges_from_network_uci +uci commit + +sync + +exit 0