dsaconfig: introduce package for UCI configuration of VLAN filter rules
authorJo-Philipp Wich <jo@mein.io>
Tue, 7 Jul 2020 13:34:53 +0000 (15:34 +0200)
committerJo-Philipp Wich <jo@mein.io>
Thu, 6 Aug 2020 06:34:19 +0000 (08:34 +0200)
This package provides the necessary files to translate `config dsa_vlan`
and `config dsa_port` sections  of `/etc/config/network` into appropriate
bridge vlan filter rules.

The approach of the configuration is to bridge all DSA ports into a logical
bridge device, called "switch0" by default, and to set VLAN port membership,
tagging state and PVID as specified by UCI on each port and on the switch
bridge device itself, allowing logical interfaces to reference port VLAN
groups by using "switch0.N" as ifname, where N denotes the VLAN ID.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
package/network/config/dsaconfig/Makefile [new file with mode: 0644]
package/network/config/dsaconfig/files/dsaconfig.hotplug [new file with mode: 0644]
package/network/config/dsaconfig/files/dsaconfig.include [new file with mode: 0755]
package/network/config/dsaconfig/files/dsaconfig.sh [new file with mode: 0755]

diff --git a/package/network/config/dsaconfig/Makefile b/package/network/config/dsaconfig/Makefile
new file mode 100644 (file)
index 0000000..4069022
--- /dev/null
@@ -0,0 +1,40 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=dsaconfig
+PKG_RELEASE:=1
+PKG_LICENSE:=GPL-2.0
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/dsaconfig
+  SECTION:=net
+  CATEGORY:=Network
+  MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
+  TITLE:=UCI configuration support for DSA switch VLAN filtering
+  DEPENDS:= +ip-bridge +ip-full
+  PKGARCH:=all
+endef
+
+define Package/dsaconfig/description
+ This package provides UCI configuration abstraction for VLAN filter rules
+ on top of DSA switches.
+endef
+
+define Build/Compile
+endef
+
+define Build/Configure
+endef
+
+define Package/dsaconfig/install
+       $(INSTALL_DIR) $(1)/etc/hotplug.d/iface
+       $(INSTALL_DATA) ./files/dsaconfig.hotplug $(1)/etc/hotplug.d/iface/01-dsaconfig
+
+       $(INSTALL_DIR) $(1)/lib/network
+       $(INSTALL_DATA) ./files/dsaconfig.include $(1)/lib/network/dsaconfig.sh
+
+       $(INSTALL_DIR) $(1)/sbin
+       $(INSTALL_BIN) ./files/dsaconfig.sh $(1)/sbin/dsaconfig
+endef
+
+$(eval $(call BuildPackage,dsaconfig))
diff --git a/package/network/config/dsaconfig/files/dsaconfig.hotplug b/package/network/config/dsaconfig/files/dsaconfig.hotplug
new file mode 100644 (file)
index 0000000..fc2a489
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if [ "$ACTION" = ifup ] && [ "$INTERFACE" = loopback ]; then
+       . /lib/network/dsaconfig.sh
+       setup_switch
+fi
+
diff --git a/package/network/config/dsaconfig/files/dsaconfig.include b/package/network/config/dsaconfig/files/dsaconfig.include
new file mode 100755 (executable)
index 0000000..4ac11d8
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+setup_switch() {
+       # Skip switch setup on network restart. The netifd process
+       # will be started afterwards and remove all interfaces again...
+       if [ "$initscript" = /etc/init.d/network ] && [ "$action" = restart ]; then
+               return 0
+       fi
+
+       /sbin/dsaconfig apply 2>&1 | logger -t dsaconfig
+}
diff --git a/package/network/config/dsaconfig/files/dsaconfig.sh b/package/network/config/dsaconfig/files/dsaconfig.sh
new file mode 100755 (executable)
index 0000000..f5769bf
--- /dev/null
@@ -0,0 +1,306 @@
+#!/bin/sh
+
+. /lib/functions.sh
+. /lib/functions/network.sh
+
+switch_names=""
+
+warn() {
+       echo "$@" >&2
+}
+
+clear_port_vlans() {
+       local port=$1
+       local self=$2
+       local vlans=$(bridge vlan show dev "$port" | sed -ne 's#^[^ ]* \+\([0-9]\+\).*$#\1#p')
+
+       local vlan
+       for vlan in $vlans; do
+               bridge vlan del vid "$vlan" dev "$port" $self
+       done
+}
+
+lookup_switch() {
+       local cfg=$1
+       local swname
+
+       config_get swname "$cfg" switch
+
+       # Auto-determine switch if not specified ...
+       if [ -z "$swname" ]; then
+               case "$switch_names" in
+                       *\ *)
+                               warn "VLAN section '$cfg' does not specify a switch but multiple switches present, using first one"
+                               swname=${switch_names%% *}
+                       ;;
+                       *)
+                               swname=${switch_names}
+                       ;;
+               esac
+
+       # ... otherwise check if the referenced switch is declared
+       else
+               case " $switch_names " in
+                       *" $swname "*) : ;;
+                       *)
+                               warn "Switch '$swname' specified by VLAN section '$cfg' does not exist"
+                               return 1
+                       ;;
+               esac
+       fi
+
+       export -n "switch=$swname"
+}
+
+validate_vid() {
+       local vid=$1
+
+       case "$vid" in
+               [1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-4][0-9][0-9][0-9])
+                       if [ $vid -gt 4096 ]; then
+                               return 1
+                       fi
+               ;;
+               *)
+                       return 1
+               ;;
+       esac
+
+       return 0
+}
+
+setup_switch() {
+       local cfg=$1
+       local cpu
+
+       # Read configured CPU port from uci ...
+       config_get cpu "$cfg" cpu_port
+
+       # ... if unspecified, find first CPU port
+       if [ -z "$cpu" ]; then
+               local e
+               for e in /sys/class/net/*/dsa; do
+                       if [ -d "$e" ]; then
+                               cpu=${e%/dsa}
+                               cpu=${cpu##*/}
+                               break
+                       fi
+               done
+       fi
+
+       # Bail out if we cannot determine the CPU port
+       if [ -z "$cpu" ]; then
+               warn "Unable to determine CPU port for switch '$cfg'"
+               return 1
+       fi
+
+       append switch_names "$cfg"
+
+       # Prevent netifd from picking up our switch bridge just yet
+       network_defer_device "$cfg"
+
+       # Increase MTU of CPU port to 1508 to accomodate for VID + DSA tag
+       ip link set "$cpu" mtu 1508
+       ip link set "$cpu" up
+
+       # (Re)create switch bridge device in case it is not yet set up
+       local filtering=$(cat "/sys/class/net/$cfg/bridge/vlan_filtering" 2>/dev/null)
+       if [ ${filtering:-0} != 1 ]; then
+               ip link set "$cfg" down 2>/dev/null
+               ip link delete dev "$cfg" 2>/dev/null
+               ip link add name "$cfg" type bridge
+               echo 1 > "/sys/class/net/$cfg/bridge/vlan_filtering"
+       fi
+
+       ip link set "$cfg" up
+
+       # Unbridge DSA ports and flush any VLAN filters on them, they're added back later
+       local port
+       for port in /sys/class/net/*"/upper_${cfg}"; do
+               if [ -e "$port" ]; then
+                       port=${port%/upper_*}
+                       port=${port##*/}
+
+                       ip link set "$port" nomaster
+
+                       # Unbridging the port should already clear VLANs, but be safe
+                       clear_port_vlans "$port"
+               fi
+       done
+
+       # Clear any VLANs on the switch bridge, they're added back later
+       clear_port_vlans "$cfg" self
+}
+
+setup_switch_vlan() {
+       local cfg=$1
+       local switch vlan ports
+
+       config_get switch "$cfg" switch
+       config_get vlan "$cfg" vlan
+       config_get ports "$cfg" ports
+
+       lookup_switch "$cfg" || return 1
+       validate_vid "$vlan" || {
+               warn "VLAN section '$cfg' specifies an invalid VLAN ID '$vlan'"
+               return 1
+       }
+
+       # Setup ports
+       local port tag pvid
+       for port in $ports; do
+               tag=${port#*.}
+               port=${port%.*}
+               pvid=
+
+               if [ "$tag" != "$port" ] && [ "$tag" = t ]; then
+                       tag=tagged
+               else
+                       tag=untagged
+               fi
+
+               # Add the port to the switch bridge and delete the default
+               # VLAN 1 if it is not yet joined to the switch.
+               if [ ! -e "/sys/class/net/$port/upper_$switch" ]; then
+                       ip link set dev "$port" up
+                       ip link set dev "$port" master "$switch"
+
+                       # Get rid of default VLAN 1
+                       bridge vlan del vid 1 dev "$port"
+               fi
+
+               # Promote the first untagged VLAN of this port to the PVID
+               if [ "$tag" = untagged ] && ! bridge vlan show dev "$port" | grep -qi pvid; then
+                       pvid=pvid
+               fi
+
+               # Add VLAN filter entry for port
+               bridge vlan add dev "$port" vid $vlan $pvid $tag
+       done
+
+       # Make the switch bridge itself handle the VLAN as well
+       bridge vlan add dev "$switch" self vid $vlan tagged
+}
+
+setup_switch_port() {
+       local cfg=$1
+       local switch port pvid tag
+
+       config_get port "$cfg" port
+       config_get pvid "$cfg" pvid
+
+       lookup_switch "$cfg" || return 1
+       validate_vid "$pvid" || {
+               warn "Port section '$cfg' specifies an invalid PVID '$pvid'"
+               return 1
+       }
+
+       # Disallow setting the PVID of the switch bridge itself
+       [ "$port" != "$switch" ] || {
+               warn "Port section '$cfg' must not change PVID of the switch bridge"
+               return 1
+       }
+
+       # Determine existing VLAN config
+       local vlanspec=$(bridge vlan show dev "$port" vid "$pvid" 2>/dev/null | sed -ne2p)
+       echo "$vlanspec" | grep -qi untagged && tag=untagged || tag=tagged
+
+       bridge vlan add vid "$pvid" dev "$port" pvid $tag
+}
+
+apply_config() {
+       config_load network
+       config_foreach setup_switch dsa
+
+       # If no switch is explicitely declared, synthesize switch0
+       if [ -z "$switch_names" ] && ! setup_switch switch0; then
+               warn "No DSA switches found"
+               return 1
+       fi
+
+       config_foreach setup_switch_vlan dsa_vlan
+       config_foreach setup_switch_port dsa_port
+
+       # Ready switch bridge devices
+       local switch
+       for switch in $switch_names; do
+               network_ready_device "$switch"
+       done
+}
+
+show_switch() {
+       local switch=$1
+
+       printf "Switch: %s\n" "$switch"
+       printf "VLAN/"
+
+       local port ports
+       for port in "/sys/class/net/$switch/lower_"*; do
+               port=${port##*/lower_}
+
+
+               printf " | %-5s" "$port"
+               append ports "$port"
+       done
+
+       printf " |\nLink:"
+
+       for port in $ports; do
+               local carrier=$(cat "/sys/class/net/$port/carrier")
+               local duplex=$(cat "/sys/class/net/$port/duplex")
+               local speed=$(cat "/sys/class/net/$port/speed")
+
+               if [ ${carrier:-0} -eq 0 ]; then
+                       printf " | %-5s" "down"
+               else
+                       [ "$duplex" = "full" ] && duplex=F || duplex=H
+                       printf " | %4d%s" "$speed" "$duplex"
+               fi
+       done
+
+       local vlans=$(bridge vlan show dev "$switch" | sed -ne 's#^[^ ]* \+\([0-9]\+\).*$#\1#p')
+       local vlan
+       for vlan in $vlans; do
+               printf " |\n%4d " "$vlan"
+
+               for port in $ports; do
+                       local pvid="" utag="" word
+                       for word in $(bridge vlan show dev "$port" vid "$vlan"); do
+                               case "$word" in
+                                       PVID) pvid="*" ;;
+                                       "$vlan") utag="t" ;;
+                                       Untagged) utag="u" ;;
+                               esac
+                       done
+
+                       printf " |  %-2s  " "$utag$pvid"
+               done
+       done
+
+       printf " |\n\n"
+}
+
+add_switch() {
+       append switch_names "$1"
+}
+
+show_config() {
+       config_load network
+       config_foreach add_switch dsa
+
+       local switch
+       for switch in ${switch_names:-switch0}; do
+               show_switch "$switch"
+       done
+}
+
+
+case "$1" in
+       show) show_config ;;
+       apply) apply_config ;;
+       *)
+               echo "Usage: ${0##*/} show"
+               echo "       ${0##*/} apply"
+               exit 1
+       ;;
+esac