aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Welte <laforge@osmocom.org>2021-03-30 22:47:56 +0200
committerHarald Welte <laforge@osmocom.org>2021-03-31 10:17:14 +0200
commit6f47979bdd51f851e385c41dcd25bc8a8dcb588a (patch)
tree91a3bdc7c46286faf661d11131fec7db480d1e91
parent90bf14ed06c9c9e365dfa4a2ad784db781183458 (diff)
Add ttcn3-ns-test-fr container
... which requires some modifications to osmo-ns-master regarding pipework / docker-entrypoint.sh Change-Id: I418f81eb1fbb2b15335ef64a3aa04d1c98a452c9 Related: SYS#5396
-rw-r--r--osmo-ns-master/Dockerfile7
-rwxr-xr-xosmo-ns-master/docker-entrypoint.sh9
-rwxr-xr-xosmo-ns-master/pipework489
l---------ttcn3-ns-test-fr/fr1
l---------ttcn3-ns-test-fr/jenkins.sh1
l---------ttcn3-ns-test-fr/netdev-to-docker.sh1
-rw-r--r--ttcn3-ns-test/fr/NS_Tests.cfg56
-rw-r--r--ttcn3-ns-test/fr/osmo-ns-dummy.cfg55
-rwxr-xr-xttcn3-ns-test/jenkins-fr.sh73
9 files changed, 691 insertions, 1 deletions
diff --git a/osmo-ns-master/Dockerfile b/osmo-ns-master/Dockerfile
index 283c410..1185253 100644
--- a/osmo-ns-master/Dockerfile
+++ b/osmo-ns-master/Dockerfile
@@ -39,7 +39,12 @@ VOLUME /data
COPY osmo-ns-dummy.cfg /data/osmo-ns-dummy.cfg
+# work-around for stupid docker not being able to properly deal with host netdevices or start
+# containers in pre-existing netns
+COPY pipework /usr/bin/pipework
+COPY docker-entrypoint.sh /docker-entrypoint.sh
+
WORKDIR /data
-CMD ["/usr/local/bin/osmo-ns-dummy", "-p", "13245"]
+CMD ["/docker-entrypoint.sh"]
#EXPOSE
diff --git a/osmo-ns-master/docker-entrypoint.sh b/osmo-ns-master/docker-entrypoint.sh
new file mode 100755
index 0000000..7bb480d
--- /dev/null
+++ b/osmo-ns-master/docker-entrypoint.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+set -x
+
+if [[ -n ${WAIT_FOR_NETDEV:-} ]]; then
+ /usr/bin/pipework --wait -i ${WAIT_FOR_NETDEV}
+fi
+
+/usr/local/bin/osmo-ns-dummy -c /data/osmo-ns-dummy.cfg -p 4240 >/data/osmo-ns-dummy.log 2>&1
diff --git a/osmo-ns-master/pipework b/osmo-ns-master/pipework
new file mode 100755
index 0000000..97ce66b
--- /dev/null
+++ b/osmo-ns-master/pipework
@@ -0,0 +1,489 @@
+#!/bin/sh
+# This code should (try to) follow Google's Shell Style Guide
+# (https://google.github.io/styleguide/shell.xml)
+set -e
+
+case "$1" in
+ --wait)
+ WAIT=1
+ ;;
+ --direct-phys)
+ DIRECT_PHYS=1
+ shift
+ ;;
+esac
+
+IFNAME=$1
+
+# default value set further down if not set here
+CONTAINER_IFNAME=
+if [ "$2" = "-i" ]; then
+ CONTAINER_IFNAME=$3
+ shift 2
+fi
+
+if [ "$2" = "-l" ]; then
+ LOCAL_IFNAME=$3
+ shift 2
+fi
+
+#inet or inet6
+FAMILY_FLAG="-4"
+if [ "$2" = "-a" ]; then
+ FAMILY_FLAG="-$3"
+ shift 2
+fi
+
+GUESTNAME=$2
+IPADDR=$3
+MACADDR=$4
+
+case "$MACADDR" in
+ *@*)
+ VLAN="${MACADDR#*@}"
+ VLAN="${VLAN%%@*}"
+ MACADDR="${MACADDR%%@*}"
+ ;;
+ *)
+ VLAN=
+ ;;
+esac
+
+# did they ask to generate a custom MACADDR?
+# generate the unique string
+case "$MACADDR" in
+ U:*)
+ macunique="${MACADDR#*:}"
+ # now generate a 48-bit hash string from $macunique
+ MACADDR=$(echo $macunique|md5sum|sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
+ ;;
+esac
+
+
+[ "$IPADDR" ] || [ "$WAIT" ] || {
+ echo "Syntax:"
+ echo "pipework <hostinterface> [-i containerinterface] [-l localinterfacename] [-a addressfamily] <guest> <ipaddr>/<subnet>[@default_gateway] [macaddr][@vlan]"
+ echo "pipework <hostinterface> [-i containerinterface] [-l localinterfacename] <guest> dhcp [macaddr][@vlan]"
+ echo "pipework mac:<hostinterface_macaddress> [-i containerinterface] [-l localinterfacename] [-a addressfamily] <guest> <ipaddr>/<subnet>[@default_gateway] [macaddr][@vlan]"
+ echo "pipework mac:<hostinterface_macaddress> [-i containerinterface] [-l localinterfacename] <guest> dhcp [macaddr][@vlan]"
+ echo "pipework route <guest> <route_command>"
+ echo "pipework rule <guest> <rule_command>"
+ echo "pipework tc <guest> <tc_command>"
+ echo "pipework --wait [-i containerinterface]"
+ exit 1
+}
+
+# Succeed if the given utility is installed. Fail otherwise.
+# For explanations about `which` vs `type` vs `command`, see:
+# http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212
+# (Thanks to @chenhanxiao for pointing this out!)
+installed () {
+ command -v "$1" >/dev/null 2>&1
+}
+
+# Google Styleguide says error messages should go to standard error.
+warn () {
+ echo "$@" >&2
+}
+die () {
+ status="$1"
+ shift
+ warn "$@"
+ exit "$status"
+}
+
+if echo $IFNAME | grep -q '^mac:'; then
+ mac_address=$(echo $IFNAME | cut -c5-)
+ ifmatch=
+ for iftest in /sys/class/net/*; do
+ if [ -f $iftest/address ] && [ "$mac_address" = "$(cat $iftest/address)" ]; then
+ ifmatch="$(basename $iftest)"
+ break
+ fi
+ done
+
+ if [ -z "$ifmatch" ]; then
+ die 1 "Mac address $mac_address specified for interface but no host interface matched."
+ else
+ IFNAME=$ifmatch
+ fi
+fi
+
+# First step: determine type of first argument (bridge, physical interface...),
+# Unless "--wait" is set (then skip the whole section)
+if [ -z "$WAIT" ]; then
+ if [ -d "/sys/class/net/$IFNAME" ]
+ then
+ if [ -d "/sys/class/net/$IFNAME/bridge" ]; then
+ IFTYPE=bridge
+ BRTYPE=linux
+ elif installed ovs-vsctl && ovs-vsctl list-br|grep -q "^${IFNAME}$"; then
+ IFTYPE=bridge
+ BRTYPE=openvswitch
+ elif [ "$(cat "/sys/class/net/$IFNAME/type")" -eq 32 ]; then # InfiniBand IPoIB interface type 32
+ IFTYPE=ipoib
+ # The IPoIB kernel module is fussy, set device name to ib0 if not overridden
+ CONTAINER_IFNAME=${CONTAINER_IFNAME:-ib0}
+ PKEY=$VLAN
+ else IFTYPE=phys
+ fi
+ else
+ case "$IFNAME" in
+ br*)
+ IFTYPE=bridge
+ BRTYPE=linux
+ ;;
+ ovs*)
+ if ! installed ovs-vsctl; then
+ die 1 "Need OVS installed on the system to create an ovs bridge"
+ fi
+ IFTYPE=bridge
+ BRTYPE=openvswitch
+ ;;
+ route*)
+ IFTYPE=route
+ ;;
+ rule*)
+ IFTYPE=rule
+ ;;
+ tc*)
+ IFTYPE=tc
+ ;;
+ dummy*)
+ IFTYPE=dummy
+ ;;
+ *) die 1 "I do not know how to setup interface $IFNAME." ;;
+ esac
+ fi
+fi
+
+# Set the default container interface name to eth1 if not already set
+CONTAINER_IFNAME=${CONTAINER_IFNAME:-eth1}
+
+[ "$WAIT" ] && {
+ while true; do
+ # This first method works even without `ip` or `ifconfig` installed,
+ # but doesn't work on older kernels (e.g. CentOS 6.X). See #128.
+ grep -q '^1$' "/sys/class/net/$CONTAINER_IFNAME/carrier" && break
+ # This method hopefully works on those older kernels.
+ ip link ls dev "$CONTAINER_IFNAME" && break
+ sleep 1
+ done > /dev/null 2>&1
+ exit 0
+}
+
+[ "$IFTYPE" = bridge ] && [ "$BRTYPE" = linux ] && [ "$VLAN" ] && {
+ die 1 "VLAN configuration currently unsupported for Linux bridge."
+}
+
+[ "$IFTYPE" = ipoib ] && [ "$MACADDR" ] && {
+ die 1 "MACADDR configuration unsupported for IPoIB interfaces."
+}
+
+# Second step: find the guest (for now, we only support LXC containers)
+while read _ mnt fstype options _; do
+ [ "$fstype" != "cgroup" ] && continue
+ echo "$options" | grep -qw devices || continue
+ CGROUPMNT=$mnt
+done < /proc/mounts
+
+[ "$CGROUPMNT" ] || {
+ die 1 "Could not locate cgroup mount point."
+}
+
+# Try to find a cgroup matching exactly the provided name.
+M=$(find "$CGROUPMNT" -name "$GUESTNAME")
+N=$(echo "$M" | wc -l)
+if [ -z "$M" ] ; then
+ N=0
+fi
+case "$N" in
+ 0)
+ # If we didn't find anything, try to lookup the container with Docker.
+ if installed docker; then
+ RETRIES=3
+ while [ "$RETRIES" -gt 0 ]; do
+ DOCKERPID=$(docker inspect --format='{{ .State.Pid }}' "$GUESTNAME")
+ DOCKERCID=$(docker inspect --format='{{ .ID }}' "$GUESTNAME")
+ DOCKERCNAME=$(docker inspect --format='{{ .Name }}' "$GUESTNAME")
+
+ [ "$DOCKERPID" != 0 ] && break
+ sleep 1
+ RETRIES=$((RETRIES - 1))
+ done
+
+ [ "$DOCKERPID" = 0 ] && {
+ die 1 "Docker inspect returned invalid PID 0"
+ }
+
+ [ "$DOCKERPID" = "<no value>" ] && {
+ die 1 "Container $GUESTNAME not found, and unknown to Docker."
+ }
+ else
+ die 1 "Container $GUESTNAME not found, and Docker not installed."
+ fi
+ ;;
+ 1) true ;;
+ 2) # LXC >=3.1.0 returns two entries from the cgroups mount instead of one.
+ echo "$M" | grep -q "lxc\.monitor" || die 1 "Found more than one container matching $GUESTNAME."
+ ;;
+ *) die 1 "Found more than one container matching $GUESTNAME." ;;
+esac
+
+# only check IPADDR if we are not in a route mode
+[ "$IFTYPE" != route ] && [ "$IFTYPE" != rule ] && [ "$IFTYPE" != tc ] && {
+ case "$IPADDR" in
+ # Let's check first if the user asked for DHCP allocation.
+ dhcp|dhcp:*)
+ # Use Docker-specific strategy to run the DHCP client
+ # from the busybox image, in the network namespace of
+ # the container.
+ if ! [ "$DOCKERPID" ]; then
+ warn "You asked for a Docker-specific DHCP method."
+ warn "However, $GUESTNAME doesn't seem to be a Docker container."
+ warn "Try to replace 'dhcp' with another option?"
+ die 1 "Aborting."
+ fi
+ DHCP_CLIENT=${IPADDR%%:*}
+ ;;
+ udhcpc|udhcpc:*|udhcpc-f|udhcpc-f:*|dhcpcd|dhcpcd:*|dhclient|dhclient:*|dhclient-f|dhclient-f:*)
+ DHCP_CLIENT=${IPADDR%%:*}
+ # did they ask for the client to remain?
+ DHCP_FOREGROUND=
+ [ "${DHCP_CLIENT%-f}" != "${DHCP_CLIENT}" ] && {
+ DHCP_FOREGROUND=true
+ }
+ DHCP_CLIENT=${DHCP_CLIENT%-f}
+ if ! installed "$DHCP_CLIENT"; then
+ die 1 "You asked for DHCP client $DHCP_CLIENT, but I can't find it."
+ fi
+ ;;
+ # Alright, no DHCP? Then let's see if we have a subnet *and* gateway.
+ */*@*)
+ GATEWAY="${IPADDR#*@}" GATEWAY="${GATEWAY%%@*}"
+ IPADDR="${IPADDR%%@*}"
+ ;;
+ # No gateway? We need at least a subnet, anyway!
+ */*) : ;;
+ # ... No? Then stop right here.
+ *)
+ warn "The IP address should include a netmask."
+ die 1 "Maybe you meant $IPADDR/24 ?"
+ ;;
+ esac
+}
+
+# If a DHCP method was specified, extract the DHCP options.
+if [ "$DHCP_CLIENT" ]; then
+ case "$IPADDR" in
+ *:*) DHCP_OPTIONS="${IPADDR#*:}" ;;
+ esac
+fi
+
+if [ "$DOCKERPID" ]; then
+ NSPID=$DOCKERPID
+else
+ NSPID=$(head -n 1 "$(find "$CGROUPMNT" -name "$GUESTNAME" | head -n 1)/tasks")
+ [ "$NSPID" ] || {
+ # it is an alternative way to get the pid
+ NSPID=$(lxc-info -n "$GUESTNAME" | grep PID | grep -Eo '[0-9]+')
+ [ "$NSPID" ] || {
+ die 1 "Could not find a process inside container $GUESTNAME."
+ }
+ }
+fi
+
+# Check if an incompatible VLAN device already exists
+[ "$IFTYPE" = phys ] && [ "$VLAN" ] && [ -d "/sys/class/net/$IFNAME.VLAN" ] && {
+ ip -d link show "$IFNAME.$VLAN" | grep -q "vlan.*id $VLAN" || {
+ die 1 "$IFNAME.VLAN already exists but is not a VLAN device for tag $VLAN"
+ }
+}
+
+[ ! -d /var/run/netns ] && mkdir -p /var/run/netns
+rm -f "/var/run/netns/$NSPID"
+ln -s "/proc/$NSPID/ns/net" "/var/run/netns/$NSPID"
+
+# Check if we need to create a bridge.
+[ "$IFTYPE" = bridge ] && [ ! -d "/sys/class/net/$IFNAME" ] && {
+ [ "$BRTYPE" = linux ] && {
+ (ip link add dev "$IFNAME" type bridge > /dev/null 2>&1) || (brctl addbr "$IFNAME")
+ ip link set "$IFNAME" up
+ }
+ [ "$BRTYPE" = openvswitch ] && {
+ ovs-vsctl add-br "$IFNAME"
+ }
+}
+
+[ "$IFTYPE" != "route" ] && [ "$IFTYPE" != "dummy" ] && [ "$IFTYPE" != "rule" ] && [ "$IFTYPE" != "tc" ] && MTU=$(ip link show "$IFNAME" | awk '{print $5}')
+
+# If it's a bridge, we need to create a veth pair
+[ "$IFTYPE" = bridge ] && {
+ if [ -z "$LOCAL_IFNAME" ]; then
+ LOCAL_IFNAME="v${CONTAINER_IFNAME}pl${NSPID}"
+ fi
+ GUEST_IFNAME="v${CONTAINER_IFNAME}pg${NSPID}"
+ # Does the link already exist?
+ if ip link show "$LOCAL_IFNAME" >/dev/null 2>&1; then
+ # link exists, is it in use?
+ if ip link show "$LOCAL_IFNAME" up | grep -q "UP"; then
+ echo "Link $LOCAL_IFNAME exists and is up"
+ exit 1
+ fi
+ # delete the link so we can re-add it afterwards
+ ip link del "$LOCAL_IFNAME"
+ fi
+ ip link add name "$LOCAL_IFNAME" mtu "$MTU" type veth peer name "$GUEST_IFNAME" mtu "$MTU"
+ case "$BRTYPE" in
+ linux)
+ (ip link set "$LOCAL_IFNAME" master "$IFNAME" > /dev/null 2>&1) || (brctl addif "$IFNAME" "$LOCAL_IFNAME")
+ ;;
+ openvswitch)
+ if ! ovs-vsctl list-ports "$IFNAME" | grep -q "^${LOCAL_IFNAME}$"; then
+ ovs-vsctl add-port "$IFNAME" "$LOCAL_IFNAME" ${VLAN:+tag="$VLAN"} \
+ -- set Interface "$LOCAL_IFNAME" \
+ external-ids:pipework.interface="$LOCAL_IFNAME" \
+ external-ids:pipework.bridge="$IFNAME" \
+ ${DOCKERCID:+external-ids:pipework.containerid="$DOCKERCID"} \
+ ${DOCKERCNAME:+external-ids:pipework.containername="$DOCKERCNAME"} \
+ ${NSPID:+external-ids:pipework.nspid="$NSPID"} \
+ ${VLAN:+external-ids:pipework.vlan="$VLAN"}
+ fi
+ ;;
+ esac
+ ip link set "$LOCAL_IFNAME" up
+}
+
+# If it's a physical interface, create a macvlan subinterface
+[ "$IFTYPE" = phys ] && {
+ [ "$VLAN" ] && {
+ [ ! -d "/sys/class/net/${IFNAME}.${VLAN}" ] && {
+ ip link add link "$IFNAME" name "$IFNAME.$VLAN" mtu "$MTU" type vlan id "$VLAN"
+ }
+ ip link set "$IFNAME" up
+ IFNAME=$IFNAME.$VLAN
+ }
+
+ if [ ! -z "$DIRECT_PHYS" ]; then
+ GUEST_IFNAME=$IFNAME
+ else
+ GUEST_IFNAME=ph$NSPID$CONTAINER_IFNAME
+ ip link add link "$IFNAME" dev "$GUEST_IFNAME" mtu "$MTU" type macvlan mode bridge
+ fi
+
+ ip link set "$IFNAME" up
+}
+
+# If it's an IPoIB interface, create a virtual IPoIB interface (the IPoIB
+# equivalent of a macvlan device)
+#
+# Note: no macvlan subinterface nor Ethernet bridge can be created on top of an
+# IPoIB interface. InfiniBand is not Ethernet. IPoIB is an IP layer on top of
+# InfiniBand, without an intermediate Ethernet layer.
+[ "$IFTYPE" = ipoib ] && {
+ GUEST_IFNAME="${IFNAME}.${NSPID}"
+
+ # If a partition key is provided, use it
+ [ "$PKEY" ] && {
+ GUEST_IFNAME="${IFNAME}.${PKEY}.${NSPID}"
+ PKEY="pkey 0x$PKEY"
+ }
+
+ ip link add link "$IFNAME" name "$GUEST_IFNAME" type ipoib $PKEY
+ ip link set "$IFNAME" up
+}
+
+# If its a dummy interface, create a dummy interface.
+[ "$IFTYPE" = dummy ] && {
+ GUEST_IFNAME=du$NSPID$CONTAINER_IFNAME
+ ip link add dev "$GUEST_IFNAME" type dummy
+}
+
+# If the `route` command was specified ...
+if [ "$IFTYPE" = route ]; then
+ # ... discard the first two arguments and pass the rest to the route command.
+ shift 2
+ ip netns exec "$NSPID" ip route "$@"
+elif [ "$IFTYPE" = rule ] ; then
+ shift 2
+ ip netns exec "$NSPID" ip rule "$@"
+elif [ "$IFTYPE" = tc ] ; then
+ shift 2
+ ip netns exec "$NSPID" tc "$@"
+else
+ # Otherwise, run normally.
+ ip link set "$GUEST_IFNAME" netns "$NSPID"
+ ip netns exec "$NSPID" ip link set "$GUEST_IFNAME" name "$CONTAINER_IFNAME"
+ [ "$MACADDR" ] && ip netns exec "$NSPID" ip link set dev "$CONTAINER_IFNAME" address "$MACADDR"
+
+ # When using any of the DHCP methods, we start a DHCP client in the
+ # network namespace of the container. With the 'dhcp' method, the
+ # client used is taken from the Docker busybox image (therefore
+ # requiring no specific client installed on the host). Other methods
+ # use a locally installed client.
+ case "$DHCP_CLIENT" in
+ dhcp)
+ docker run -d --net container:$GUESTNAME --cap-add NET_ADMIN \
+ busybox udhcpc -i "$CONTAINER_IFNAME" -x "hostname:$GUESTNAME" \
+ $DHCP_OPTIONS \
+ >/dev/null
+ ;;
+ udhcpc)
+ DHCP_Q="-q"
+ [ "$DHCP_FOREGROUND" ] && {
+ DHCP_OPTIONS="$DHCP_OPTIONS -f"
+ }
+ ip netns exec "$NSPID" "$DHCP_CLIENT" -qi "$CONTAINER_IFNAME" \
+ -x "hostname:$GUESTNAME" \
+ -p "/var/run/udhcpc.$GUESTNAME.pid" \
+ $DHCP_OPTIONS
+ [ ! "$DHCP_FOREGROUND" ] && {
+ rm "/var/run/udhcpc.$GUESTNAME.pid"
+ }
+ ;;
+ dhclient)
+ ip netns exec "$NSPID" "$DHCP_CLIENT" "$CONTAINER_IFNAME" \
+ -pf "/var/run/dhclient.$GUESTNAME.pid" \
+ -lf "/etc/dhclient/dhclient.$GUESTNAME.leases" \
+ $DHCP_OPTIONS
+ # kill dhclient after get ip address to prevent device be used after container close
+ [ ! "$DHCP_FOREGROUND" ] && {
+ kill "$(cat "/var/run/dhclient.$GUESTNAME.pid")"
+ rm "/var/run/dhclient.$GUESTNAME.pid"
+ }
+ ;;
+ dhcpcd)
+ ip netns exec "$NSPID" "$DHCP_CLIENT" -q "$CONTAINER_IFNAME" -h "$GUESTNAME"
+ ;;
+ "")
+ if installed ipcalc; then
+ eval $(ipcalc -b $IPADDR)
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" addr add "$IPADDR" brd "$BROADCAST" dev "$CONTAINER_IFNAME"
+ else
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" addr add "$IPADDR" dev "$CONTAINER_IFNAME"
+ fi
+
+ [ "$GATEWAY" ] && {
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" route delete default >/dev/null 2>&1 && true
+ }
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" link set "$CONTAINER_IFNAME" up
+ [ "$GATEWAY" ] && {
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" route get "$GATEWAY" >/dev/null 2>&1 || \
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" route add "$GATEWAY/32" dev "$CONTAINER_IFNAME"
+ ip netns exec "$NSPID" ip "$FAMILY_FLAG" route replace default via "$GATEWAY" dev "$CONTAINER_IFNAME"
+ }
+ ;;
+ esac
+
+ # Give our ARP neighbors a nudge about the new interface
+ if installed arping; then
+ IPADDR=$(echo "$IPADDR" | cut -d/ -f1)
+ ip netns exec "$NSPID" arping -c 1 -A -I "$CONTAINER_IFNAME" "$IPADDR" > /dev/null 2>&1 || true
+ else
+ echo "Warning: arping not found; interface may not be immediately reachable"
+ fi
+fi
+# Remove NSPID to avoid `ip netns` catch it.
+rm -f "/var/run/netns/$NSPID"
+
+# vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab :
diff --git a/ttcn3-ns-test-fr/fr b/ttcn3-ns-test-fr/fr
new file mode 120000
index 0000000..ce2797b
--- /dev/null
+++ b/ttcn3-ns-test-fr/fr
@@ -0,0 +1 @@
+../ttcn3-ns-test/fr \ No newline at end of file
diff --git a/ttcn3-ns-test-fr/jenkins.sh b/ttcn3-ns-test-fr/jenkins.sh
new file mode 120000
index 0000000..19f578b
--- /dev/null
+++ b/ttcn3-ns-test-fr/jenkins.sh
@@ -0,0 +1 @@
+../ttcn3-ns-test/jenkins-fr.sh \ No newline at end of file
diff --git a/ttcn3-ns-test-fr/netdev-to-docker.sh b/ttcn3-ns-test-fr/netdev-to-docker.sh
new file mode 120000
index 0000000..3ae2cf8
--- /dev/null
+++ b/ttcn3-ns-test-fr/netdev-to-docker.sh
@@ -0,0 +1 @@
+../ttcn3-gbproxy-test-fr/netdev-to-docker.sh \ No newline at end of file
diff --git a/ttcn3-ns-test/fr/NS_Tests.cfg b/ttcn3-ns-test/fr/NS_Tests.cfg
new file mode 100644
index 0000000..5ece397
--- /dev/null
+++ b/ttcn3-ns-test/fr/NS_Tests.cfg
@@ -0,0 +1,56 @@
+[ORDERED_INCLUDE]
+"/osmo-ttcn3-hacks/Common.cfg"
+"/osmo-ttcn3-hacks/ns/NS_Tests.default"
+
+[LOGGING]
+
+[TESTPORT_PARAMETERS]
+*.NSVTY.CTRL_HOSTNAME := "172.18.30.101"
+
+[MODULE_PARAMETERS]
+NS_Tests.mp_dialect := NS2_DIALECT_IPACCESS
+NS_Tests.mp_nsconfig := {
+ nsei := 2001,
+ role_sgsn := false,
+ handle_sns := false,
+ nsvc := {
+ {
+ provider := {
+ fr := {
+ netdev := "hdlc1",
+ dlci := 16
+ }
+ },
+ nsvci := 1
+ }, {
+ provider := {
+ fr := {
+ netdev := "hdlc2",
+ dlci := 17
+ }
+ },
+ nsvci := 2
+ }, {
+ provider := {
+ fr := {
+ netdev := "hdlc3",
+ dlci := 18
+ }
+ },
+ nsvci := 3
+ }, {
+ provider := {
+ fr := {
+ netdev := "hdlc4",
+ dlci := 19
+ }
+ },
+ nsvci := 4
+ }
+ }
+}
+
+[MAIN_CONTROLLER]
+
+[EXECUTE]
+NS_Tests.control
diff --git a/ttcn3-ns-test/fr/osmo-ns-dummy.cfg b/ttcn3-ns-test/fr/osmo-ns-dummy.cfg
new file mode 100644
index 0000000..0a3515d
--- /dev/null
+++ b/ttcn3-ns-test/fr/osmo-ns-dummy.cfg
@@ -0,0 +1,55 @@
+log gsmtap 172.18.30.10
+ logging level set-all debug
+
+log stderr
+ logging filter all 1
+ logging print level 1
+ logging print category 1
+ logging print category-hex 0
+ logging print file basename last
+ logging print extended-timestamp 1
+ logging level set-all debug
+!
+stats interval 0
+stats reporter statsd
+ prefix TTCN3
+ level subscriber
+ remote-ip 172.18.30.10
+ remote-port 8125
+ flush-period 1
+ mtu 1024
+ enable
+
+line vty
+ no login
+ bind 172.18.30.101
+
+ns
+ bind fr hdlcnet1
+ fr hdlcnet1 frnet
+ bind fr hdlcnet2
+ fr hdlcnet2 frnet
+ bind fr hdlcnet3
+ fr hdlcnet3 frnet
+ bind fr hdlcnet4
+ fr hdlcnet4 frnet
+ bind fr hdlcnet5
+ fr hdlcnet5 frnet
+ bind fr hdlcnet6
+ fr hdlcnet6 frnet
+ bind fr hdlcnet7
+ fr hdlcnet7 frnet
+ bind fr hdlcnet8
+ fr hdlcnet8 frnet
+ nse 2001
+ nsvc fr hdlcnet1 dlci 16 nsvci 1
+ nsvc fr hdlcnet2 dlci 17 nsvci 2
+ nsvc fr hdlcnet3 dlci 18 nsvci 3
+ nsvc fr hdlcnet4 dlci 19 nsvci 4
+ nse 2002
+ nsvc fr hdlcnet5 dlci 20 nsvci 5
+ nsvc fr hdlcnet6 dlci 21 nsvci 6
+ nse 2003
+ nsvc fr hdlcnet7 dlci 22 nsvci 7
+ nsvc fr hdlcnet8 dlci 23 nsvci 8
+
diff --git a/ttcn3-ns-test/jenkins-fr.sh b/ttcn3-ns-test/jenkins-fr.sh
new file mode 100755
index 0000000..df23a35
--- /dev/null
+++ b/ttcn3-ns-test/jenkins-fr.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+# WARNING: This cannot be executed on any random Linux machine or jenkins slave node!
+#
+# We require a kernel with HLDC net-devices, specifically eight pairs of devices named
+# hdlc1 + hdlcnet1 ... hdlc8 + hdclnet8.
+#
+# Those pairs of netdevices can e.g. be implemented by actually physically looping back
+# the related E1 interfaces, or e.g.by using DAHDI_NET + dahdi_dynamic_loc to create
+# pairs of virtual E1 spans.
+#
+# In addition, we need to use 'sudo' permissions in order to move the hdlc
+# net-devices into the docker containers. So in automatic test execution this means
+# that the user will need sudo privileges without entering a password (NOPASS)
+
+. ../jenkins-common.sh
+IMAGE_SUFFIX="${IMAGE_SUFFIX:-master}"
+docker_images_require \
+ "osmo-ns-$IMAGE_SUFFIX" \
+ "ttcn3-ns-test"
+
+set_clean_up_trap
+set -e
+
+SUBNET=30
+network_create $SUBNET
+
+mkdir $VOL_BASE_DIR/ns-tester
+cp fr/NS_Tests.cfg $VOL_BASE_DIR/ns-tester/
+
+mkdir $VOL_BASE_DIR/ns
+cp fr/osmo-ns-dummy.cfg $VOL_BASE_DIR/ns/
+
+echo Starting container with osmo-ns-dummy
+docker run --rm \
+ --cap-add=NET_RAW --cap-add=SYS_RAWIO \
+ $(docker_network_params $SUBNET 101) \
+ --ulimit core=-1 \
+ -e "WAIT_FOR_NETDEV=hdlcnet8" \
+ -v $VOL_BASE_DIR/ns:/data \
+ --name ${BUILD_TAG}-ns -d \
+ $DOCKER_ARGS \
+ $REPO_USER/osmo-ns-$IMAGE_SUFFIX
+
+# move all hdlcX net-devices into container
+for i in `seq 1 8`; do
+ DEV="hdlcnet$i"
+ #sudo sethdlc ${DEV} fr lmi none
+ sudo ./netdev-to-docker.sh ${DEV} ${BUILD_TAG}-ns
+done
+
+echo Starting container with NS testsuite
+docker run --rm \
+ --cap-add=NET_RAW --cap-add=SYS_RAWIO \
+ $(docker_network_params $SUBNET 10) \
+ --ulimit core=-1 \
+ -e "TTCN3_PCAP_PATH=/data" \
+ -e "WAIT_FOR_NETDEV=hdlc8" \
+ -v $VOL_BASE_DIR/ns-tester:/data \
+ --name ${BUILD_TAG}-ttcn3-ns-test -d \
+ $DOCKER_ARGS \
+ $REPO_USER/ttcn3-ns-test
+
+# move all hdlcnetX net-devices into container
+for i in `seq 1 8`; do
+ DEV="hdlc$i"
+ #sudo sethdlc ${DEV} fr lmi none
+ sudo ./netdev-to-docker.sh ${DEV} ${BUILD_TAG}-ttcn3-ns-test
+done
+
+# emulate running container in foreground, which is no longer possible as we
+# must shift the net-devices into the container _after_ it is started
+docker logs -f ${BUILD_TAG}-ttcn3-ns-test