diff --git a/package/network/config/dsaconfig/Makefile b/package/network/config/dsaconfig/Makefile
new file mode 100644
index 0000000000..4069022224
--- /dev/null
+++ b/package/network/config/dsaconfig/Makefile
@@ -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
index 0000000000..fc2a489dea
--- /dev/null
+++ b/package/network/config/dsaconfig/files/dsaconfig.hotplug
@@ -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
index 0000000000..4ac11d8061
--- /dev/null
+++ b/package/network/config/dsaconfig/files/dsaconfig.include
@@ -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
index 0000000000..0182f340ba
--- /dev/null
+++ b/package/network/config/dsaconfig/files/dsaconfig.sh
@@ -0,0 +1,296 @@
+#!/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
+		[0-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/master" ]; 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 switch
+
+	# 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 switch_vlan
+	config_foreach setup_switch_port switch_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="t" word
+			for word in $(bridge vlan show dev "$port" vid "$vlan"); do
+				case "$word" in
+					PVID) pvid="*" ;;
+					Untagged) utag="u" ;;
+				esac
+			done
+
+			printf " |  %-2s  " "$utag$pvid"
+		done
+	done
+
+	printf " |\n\n"
+}
+
+show_config() {
+	config_load network
+	config_foreach show_switch switch
+}
+
+
+case "$1" in
+	show) show_config ;;
+	apply) apply_config ;;
+	*)
+		echo "Usage: ${0##*/} show"
+		echo "       ${0##*/} apply"
+		exit 1
+	;;
+esac
