--- /dev/null
+Motivation
+==========
+
+One of the nice things about network namespaces is that they allow one
+to easily create and test complex environments.
+
+Unfortunately, these namespaces can not be used with actual switching
+ASICs, as their ports can not be migrated to other network namespaces
+(NETIF_F_NETNS_LOCAL) and most of them probably do not support the
+L1-separation provided by namespaces.
+
+However, a similar kind of flexibility can be achieved by using VRFs and
+by looping the switch ports together. For example:
+
+                             br0
+                              +
+               vrf-h1         |           vrf-h2
+                 +        +---+----+        +
+                 |        |        |        |
+    192.0.2.1/24 +        +        +        + 192.0.2.2/24
+               swp1     swp2     swp3     swp4
+                 +        +        +        +
+                 |        |        |        |
+                 +--------+        +--------+
+
+The VRFs act as lightweight namespaces representing hosts connected to
+the switch.
+
+This approach for testing switch ASICs has several advantages over the
+traditional method that requires multiple physical machines, to name a
+few:
+
+1. Only the device under test (DUT) is being tested without noise from
+other system.
+
+2. Ability to easily provision complex topologies. Testing bridging
+between 4-ports LAGs or 8-way ECMP requires many physical links that are
+not always available. With the VRF-based approach one merely needs to
+loopback more ports.
+
+These tests are written with switch ASICs in mind, but they can be run
+on any Linux box using veth pairs to emulate physical loopbacks.
+
+Guidelines for Writing Tests
+============================
+
+o Where possible, reuse an existing topology for different tests instead
+  of recreating the same topology.
+o Where possible, IPv6 and IPv4 addresses shall conform to RFC 3849 and
+  RFC 5737, respectively.
+o Where possible, tests shall be written so that they can be reused by
+  multiple topologies and added to lib.sh.
+o Checks shall be added to lib.sh for any external dependencies.
+o Code shall be checked using ShellCheck [1] prior to submission.
+
+1. https://www.shellcheck.net/
 
--- /dev/null
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+##############################################################################
+# Defines
+
+# Can be overridden by the configuration file.
+PING=${PING:=ping}
+PING6=${PING6:=ping6}
+WAIT_TIME=${WAIT_TIME:=5}
+PAUSE_ON_FAIL=${PAUSE_ON_FAIL:=no}
+PAUSE_ON_CLEANUP=${PAUSE_ON_CLEANUP:=no}
+
+if [[ -f forwarding.config ]]; then
+       source forwarding.config
+fi
+
+##############################################################################
+# Sanity checks
+
+if [[ "$(id -u)" -ne 0 ]]; then
+       echo "SKIP: need root privileges"
+       exit 0
+fi
+
+tc -j &> /dev/null
+if [[ $? -ne 0 ]]; then
+       echo "SKIP: iproute2 too old, missing JSON support"
+       exit 0
+fi
+
+if [[ ! -x "$(command -v jq)" ]]; then
+       echo "SKIP: jq not installed"
+       exit 0
+fi
+
+if [[ ! -v NUM_NETIFS ]]; then
+       echo "SKIP: importer does not define \"NUM_NETIFS\""
+       exit 0
+fi
+
+##############################################################################
+# Network interfaces configuration
+
+for i in $(eval echo {1..$NUM_NETIFS}); do
+       ip link show dev ${NETIFS[p$i]} &> /dev/null
+       if [[ $? -ne 0 ]]; then
+               echo "SKIP: could not find all required interfaces"
+               exit 0
+       fi
+done
+
+##############################################################################
+# Helpers
+
+# Exit status to return at the end. Set in case one of the tests fails.
+EXIT_STATUS=0
+# Per-test return value. Clear at the beginning of each test.
+RET=0
+
+check_err()
+{
+       local err=$1
+       local msg=$2
+
+       if [[ $RET -eq 0 && $err -ne 0 ]]; then
+               RET=$err
+               retmsg=$msg
+       fi
+}
+
+check_fail()
+{
+       local err=$1
+       local msg=$2
+
+       if [[ $RET -eq 0 && $err -eq 0 ]]; then
+               RET=1
+               retmsg=$msg
+       fi
+}
+
+log_test()
+{
+       local test_name=$1
+       local opt_str=$2
+
+       if [[ $# -eq 2 ]]; then
+               opt_str="($opt_str)"
+       fi
+
+       if [[ $RET -ne 0 ]]; then
+               EXIT_STATUS=1
+               printf "TEST: %-60s  [FAIL]\n" "$test_name $opt_str"
+               if [[ ! -z "$retmsg" ]]; then
+                       printf "\t%s\n" "$retmsg"
+               fi
+               if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+                       echo "Hit enter to continue, 'q' to quit"
+                       read a
+                       [ "$a" = "q" ] && exit 1
+               fi
+               return 1
+       fi
+
+       printf "TEST: %-60s  [PASS]\n" "$test_name $opt_str"
+       return 0
+}
+
+setup_wait()
+{
+       for i in $(eval echo {1..$NUM_NETIFS}); do
+               while true; do
+                       ip link show dev ${NETIFS[p$i]} up \
+                               | grep 'state UP' &> /dev/null
+                       if [[ $? -ne 0 ]]; then
+                               sleep 1
+                       else
+                               break
+                       fi
+               done
+       done
+
+       # Make sure links are ready.
+       sleep $WAIT_TIME
+}
+
+pre_cleanup()
+{
+       if [ "${PAUSE_ON_CLEANUP}" = "yes" ]; then
+               echo "Pausing before cleanup, hit any key to continue"
+               read
+       fi
+}
+
+vrf_prepare()
+{
+       ip -4 rule add pref 32765 table local
+       ip -4 rule del pref 0
+       ip -6 rule add pref 32765 table local
+       ip -6 rule del pref 0
+}
+
+vrf_cleanup()
+{
+       ip -6 rule add pref 0 table local
+       ip -6 rule del pref 32765
+       ip -4 rule add pref 0 table local
+       ip -4 rule del pref 32765
+}
+
+__last_tb_id=0
+declare -A __TB_IDS
+
+__vrf_td_id_assign()
+{
+       local vrf_name=$1
+
+       __last_tb_id=$((__last_tb_id + 1))
+       __TB_IDS[$vrf_name]=$__last_tb_id
+       return $__last_tb_id
+}
+
+__vrf_td_id_lookup()
+{
+       local vrf_name=$1
+
+       return ${__TB_IDS[$vrf_name]}
+}
+
+vrf_create()
+{
+       local vrf_name=$1
+       local tb_id
+
+       __vrf_td_id_assign $vrf_name
+       tb_id=$?
+
+       ip link add dev $vrf_name type vrf table $tb_id
+       ip -4 route add table $tb_id unreachable default metric 4278198272
+       ip -6 route add table $tb_id unreachable default metric 4278198272
+}
+
+vrf_destroy()
+{
+       local vrf_name=$1
+       local tb_id
+
+       __vrf_td_id_lookup $vrf_name
+       tb_id=$?
+
+       ip -6 route del table $tb_id unreachable default metric 4278198272
+       ip -4 route del table $tb_id unreachable default metric 4278198272
+       ip link del dev $vrf_name
+}
+
+__addr_add_del()
+{
+       local if_name=$1
+       local add_del=$2
+       local array
+
+       shift
+       shift
+       array=("${@}")
+
+       for addrstr in "${array[@]}"; do
+               ip address $add_del $addrstr dev $if_name
+       done
+}
+
+simple_if_init()
+{
+       local if_name=$1
+       local vrf_name
+       local array
+
+       shift
+       vrf_name=v$if_name
+       array=("${@}")
+
+       vrf_create $vrf_name
+       ip link set dev $if_name master $vrf_name
+       ip link set dev $vrf_name up
+       ip link set dev $if_name up
+
+       __addr_add_del $if_name add "${array[@]}"
+}
+
+simple_if_fini()
+{
+       local if_name=$1
+       local vrf_name
+       local array
+
+       shift
+       vrf_name=v$if_name
+       array=("${@}")
+
+       __addr_add_del $if_name del "${array[@]}"
+
+       ip link set dev $if_name down
+       vrf_destroy $vrf_name
+}
+
+master_name_get()
+{
+       local if_name=$1
+
+       ip -j link show dev $if_name | jq -r '.[]["master"]'
+}
+
+##############################################################################
+# Tests
+
+ping_test()
+{
+       local if_name=$1
+       local dip=$2
+       local vrf_name
+
+       RET=0
+
+       vrf_name=$(master_name_get $if_name)
+       ip vrf exec $vrf_name $PING $dip -c 10 -i 0.1 -w 2 &> /dev/null
+       check_err $?
+       log_test "ping"
+}
+
+ping6_test()
+{
+       local if_name=$1
+       local dip=$2
+       local vrf_name
+
+       RET=0
+
+       vrf_name=$(master_name_get $if_name)
+       ip vrf exec $vrf_name $PING6 $dip -c 10 -i 0.1 -w 2 &> /dev/null
+       check_err $?
+       log_test "ping6"
+}