Merge branch 'hmehdi_bridge' into 'devel'

Draft: bridgemngr: add support for bridge config in bridging UCI

See merge request feed/iopsys!2123
This commit is contained in:
Husaam Mehdi 2026-03-12 14:12:31 +00:00 committed by IOPSYS Dev
commit cd2cc94d2b
No known key found for this signature in database
3 changed files with 1050 additions and 22 deletions

View file

@ -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)

View file

@ -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 <name> <type> <vid> <ifname>
# Sets _ensured_devname to the section name used (same as <name>).
# ---------------------------------------------------------------------------
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 <varname> <value>
# ---------------------------------------------------------------------------
_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
}

View file

@ -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