* Fixed iptables invocation that broke a few commits back.
authorUrban Wallasch <urban.wallasch@freenet.de>
Tue, 23 Apr 2019 08:52:17 +0000 (10:52 +0200)
committerUrban Wallasch <urban.wallasch@freenet.de>
Tue, 23 Apr 2019 08:52:17 +0000 (10:52 +0200)
* Added DHCP support for bridge interface configuration.
* Calculate bridge network from its IP address and prefix length.
* Moved most of the configuration presets to separate nns_example.cfg file.
* Several minor fixes and improvements.

nns.sh
nns_example.cfg [new file with mode: 0644]

diff --git a/nns.sh b/nns.sh
index 2605635470364db51426f231ef2c54aade85ba4d..b0e1e3988d7c6d7a612ab16e8b14f74c5319c542 100755 (executable)
--- a/nns.sh
+++ b/nns.sh
@@ -5,43 +5,62 @@
 # the host (default) network namespace.
 #
 
-###############################
-# Default configuration; can be overriden by specifying
-# a shell script fragment as first parameter:
+######################################################################
+# To-Do:
+# ------
+#
+# * Find a nice way to keep dhclient when run within a network namespace
+#   from littering the system with /etc/resolv.conf.dhclient-new.XXXX
+#   files while attempting to overwrite /etc/resolv.conf.
+#   Current workaround is to indiscriminately remove such files.
+#   Cf: https://stackoverflow.com/questions/38102481/how-can-dhclient-be-made-namespace-aware )
+#
+#####
+
+######################################################################
+# Default configuration; can be overridden by passing the name
+# of a shell script fragment containing appropriate variable
+# assignments (see nns_example.cfg) as the first argument.
+#
+# Note:
+# - Empty *_ADDR parameters will lead to DHCP being used.
+# - Empty *_BCAST  parameters will set to '+', i.e. auto calculate.
+#
 
+# Name of network namespace:
 NNSNAME=
 
+# Base (e.g. VPN) host interface to link network namespace to:
 BASE_IF=vpn_tap1
 
-# If GUEST_ADDR is empty, DHCP is used to figure stuff out:
+# Properties of virtual interface to create in network namespace:
 GUEST_ADDR=
-GUEST_NET=192.168.30.0
-# '+' means auto calculate broadcast address:
-GUEST_BCAST=+
-GUEST_PLEN=24
-GUEST_MAC=FE:17:90:00:00:00
+GUEST_PLEN=
+GUEST_BCAST=
+GUEST_MAC=
 
+# Properties of bridge interface to connect with network namespace:
 BRIDGE_IF=br0
-BRIDGE_ADDR=192.168.30.3
-# '+' means auto calculate broadcast address:
-BRIDGE_BCAST=+
-BRIDGE_PLEN=24
-BRIDGE_MAC=FE:17:90:00:00:01
-
-# List of nameservers to provide DNS in the new network namespace;
-# If empty, host resolv.conf will be used.
-NAMESRV="8.8.8.8 8.8.4.4"
-
-# Program to run in the new network namespace:
-XCMD1=xterm
+BRIDGE_ADDR=
+BRIDGE_PLEN=
+BRIDGE_BCAST=
+BRIDGE_MAC=
+
+# Space separated list of nameservers to provide DNS in the new network
+# namespace. If empty, the host resolv.conf will be copied:
+NAMESRV=
+
+# Program with optional arguments to run in the new network namespace:
+XCMD1=
 XCMD1_ARGS=
 
 # Virtual Ethernet to connect new network namespace to default namespace:
 VETH_HOST=veth_host
 VETH_GUEST=veth_guest
 
-# Configuration end.
-###############################
+#
+# End of configuration.
+######################################################################
 
 
 #set -x
@@ -62,8 +81,8 @@ fi
 # Become root if not already:
 [ "root" != "$USER" ] && exec sudo -E $0 "$@"
 
-# Check, if first parameter looks like a config file name.
-# If so: source it. Otherwise take $1 as namespace name.
+# Check, if first argument looks like a config file name and
+# if so source it. Otherwise $1 is interpreted as namespace name.
 if [ -f "$1" ] && [ -r "$1" ] ; then
     . "$1"
 else
@@ -71,6 +90,22 @@ else
     NNSNAME=${1//[[:space:]]}
 fi
 
+# Check and or sanitize some settings:
+if [ -z "$NNSNAME" ] ; then
+    echo "No network namespace name provided."
+    exit 1
+fi
+if [ -z "$BASE_IF" ] ; then
+    echo "No base interface specified."
+    exit 1
+fi
+[ -z "$GUEST_BCAST" ] && GUEST_BCAST="+"
+[ -z "$BRIDGE_BCAST" ] && BRIDGE_BCAST="+"
+[ -z "$BRIDGE_IF" ] && BRIDGE_IF="br0"
+[ -z "$VETH_HOST" ] && VETH_HOST="veth_host"
+[ -z "$VETH_GUEST" ] && VETH_GUEST="veth_guest"
+
+
 # Locate essential commands:
 IP=$(command -v ip)
 if [ -z $IP ] ; then
@@ -82,10 +117,12 @@ if [ -z $IPTABLES ] ; then
     echo "please install the iptables package"
     exit 1
 fi
-DHCLIENT=$(command -v dhclient)
-if [ -z $DHCLIENT ] ; then
-    echo "please install the dhclient package"
-    exit 1
+if [ -z "$BRIDGE_ADDR" ] || [ -z "$GUEST_ADDR" ] ; then
+    DHCLIENT=$(command -v dhclient)
+    if [ -z $DHCLIENT ] ; then
+        echo "please install the dhclient package"
+        exit 1
+    fi
 fi
 
 # Locate user provided command:
@@ -101,6 +138,49 @@ fi
 RUNDIR=/var/run/newns/$NNSNAME
 /bin/mkdir -p $RUNDIR
 
+# Get IPv4 address/prefix_length of an interface:
+get_ip_pfl() {
+    $IP addr show $1 | grep -F 'inet ' | awk -F ' ' '{print $2}'
+}
+
+# IPv4 math helpers; shamelessly ripped from:
+# https://stackoverflow.com/questions/15429420/given-the-ip-and-netmask-how-can-i-calculate-the-network-address-using-bash#32690695
+ip2int() {
+    local a b c d
+    { IFS=. read a b c d; } <<< $1
+    echo $(((((((a << 8) | b) << 8) | c) << 8) | d))
+}
+
+int2ip() {
+    local ui32=$1; shift
+    local ip n
+    for n in 1 2 3 4; do
+        ip=$((ui32 & 0xff))${ip:+.}$ip
+        ui32=$((ui32 >> 8))
+    done
+    echo $ip
+}
+
+netmask() {
+    # Example: netmask 24 => 255.255.255.0
+    local mask=$((0xffffffff << (32 - $1))); shift
+    int2ip $mask
+}
+
+broadcast() {
+    # Example: broadcast 192.0.2.0 24 => 192.0.2.255
+    local addr=$(ip2int $1); shift
+    local mask=$((0xffffffff << (32 -$1))); shift
+    int2ip $((addr | ~mask))
+}
+
+network() {
+    # Example: network 192.0.2.0 24 => 192.0.2.0
+    local addr=$(ip2int $1); shift
+    local mask=$((0xffffffff << (32 -$1))); shift
+    int2ip $((addr & mask))
+}
+
 
 # Check whether a namespace with given name exists:
 exist_nns() {
@@ -111,8 +191,7 @@ exist_nns() {
 # Set up a new network namespace:
 start_nns() {
     if exist_nns $1 ; then
-        echo "Network namespace $1 already exists,"
-        echo "please choose another name."
+        echo "Network namespace $1 already exists, please choose another name."
         exit 1
     fi
 
@@ -134,14 +213,22 @@ start_nns() {
     # Create the new namespace, the veth cable and the bridge interface:
     $IP netns add $1
     $IP link add $VETH_HOST type veth peer name $VETH_GUEST
-    $IP link set dev $VETH_GUEST address $GUEST_MAC
+    [ -n "$GUEST_MAC" ] && $IP link set dev $VETH_GUEST address $GUEST_MAC
     $IP link add $BRIDGE_IF type bridge
-    $IP link set dev $BRIDGE_IF address $BRIDGE_MAC
+    [ -n "$BRIDGE_MAC" ] && $IP link set dev $BRIDGE_IF address $BRIDGE_MAC
     $IP link set $BASE_IF master $BRIDGE_IF
     $IP link set $VETH_HOST master $BRIDGE_IF
-    $IP addr add $BRIDGE_ADDR/$BRIDGE_PLEN broadcast $BRIDGE_BCAST dev $BRIDGE_IF
     $IP link set dev $VETH_HOST up
-    $IP link set dev $BRIDGE_IF up
+    if [ -z "$BRIDGE_ADDR" ] ; then
+        $DHCLIENT -pf "$RUNDIR/dhclient.$BRIDGE_IF.pid" -v $BRIDGE_IF
+        local IP_PFL=$(get_ip_pfl $BRIDGE_IF)
+        BRIDGE_ADDR=${IP_PFL%/*}
+        BRIDGE_PLEN=${IP_PFL#*/}
+    else
+        $IP addr add $BRIDGE_ADDR/$BRIDGE_PLEN broadcast $BRIDGE_BCAST dev $BRIDGE_IF
+        $IP link set dev $BRIDGE_IF up
+    fi
+    BRIDGE_NET=$(network $BRIDGE_ADDR $BRIDGE_PLEN)
 
     # Assign the veth guest interface to the new network namespace,
     # set an IP address (either static or via dhclient) to it, bring
@@ -149,6 +236,8 @@ start_nns() {
     $IP link set $VETH_GUEST netns $1
     if [ -z "$GUEST_ADDR" ] ; then
         $IP netns exec $1 $DHCLIENT -pf "$RUNDIR/dhclient.$VETH_GUEST.pid" -v $VETH_GUEST
+        # Remove dhclients useless remains:
+        rm /etc/resolv.conf.dhclient-new.*
     else
         $IP netns exec $1 $IP addr add $GUEST_ADDR/$GUEST_PLEN broadcast $GUEST_BCAST dev $VETH_GUEST
         $IP netns exec $1 $IP link set dev $VETH_GUEST up
@@ -156,7 +245,7 @@ start_nns() {
     $IP netns exec $1 $IP link set dev lo up
 
     # Set up routing:
-    $IPTABLES -t nat -A POSTROUTING --source $GUEST_NET --jump MASQUERADE
+    $IPTABLES -t nat -A POSTROUTING --source $BRIDGE_NET/$BRIDGE_PLEN --jump MASQUERADE
     $IP netns exec $1 $IP route add default via $BRIDGE_ADDR
 
     # Run the configured command, if any, inside the namespace:
@@ -170,13 +259,14 @@ start_nns() {
 # Tear down a previously created network namespace and clean up the mess:
 stop_nns() {
     if ! exist_nns $1 ; then
-        echo "Network namespace $1 does not exist,"
-        echo "please choose another name"
+        echo "Network namespace $1 does not exist, please choose another name."
         exit 1
     fi
 
     # Kill processes started in the separate namespace:
-    for PIDF in $RUNDIR/*.pid ; do
+    for PIDF in $(find $RUNDIR -maxdepth 1 -name '*.pid' 2> /dev/null) ; do
+        # Note: This can lead to nasty surprises in case a process
+        # already terminated and its PID has been reused!
         /bin/kill -TERM $(cat "$PIDF")
         /bin/rm "$PIDF"
     done
@@ -186,6 +276,11 @@ stop_nns() {
     rm $RUNDIR/ipv4fwd.sav
 
     # Burn the bridge:
+    IP_PFL=$(get_ip_pfl $BRIDGE_IF)
+    BRIDGE_ADDR=${IP_PFL%/*}
+    BRIDGE_PLEN=${IP_PFL#*/}
+    BRIDGE_NET=$(network $BRIDGE_ADDR $BRIDGE_PLEN)
+    $IPTABLES -t nat -D POSTROUTING --source $BRIDGE_NET/$BRIDGE_PLEN --jump MASQUERADE
     $IP link set dev $BRIDGE_IF down
     $IP link set dev $BASE_IF nomaster
     $IP link set dev $VETH_HOST nomaster
@@ -193,9 +288,8 @@ stop_nns() {
 
     # Unplug the virtual cable and remove network namespace:
     $IP netns exec $1 $IP link del $VETH_GUEST
-    $IP link del $VETH_HOST
+    $IP link del $VETH_HOST 2> /dev/null
     $IP netns del $1
-    $IPTABLES -t nat -D POSTROUTING --source $GUEST_NET --jump MASQUERADE
 
     # Cleanup namespace DNS configuration:
     /bin/rm /etc/netns/$1/resolv.conf
@@ -205,11 +299,11 @@ stop_nns() {
     /bin/rmdir $RUNDIR
 }
 
+
 # Run a specified command inside a network namespace:
 run_nns() {
     if ! exist_nns $1 ; then
-        echo "Network namespace $1 does not exist,"
-        echo "please choose another name"
+        echo "Network namespace $1 does not exist, please choose another name."
         exit 1
     fi
     NNS=$1
diff --git a/nns_example.cfg b/nns_example.cfg
new file mode 100644 (file)
index 0000000..2b55d0f
--- /dev/null
@@ -0,0 +1,38 @@
+# Example configuration for use with nns.sh script.
+#
+# Note:
+# - Empty *_ADDR parameters will lead to DHCP being used.
+# - Empty *_BCAST  parameters will set to '+', i.e. auto calculate.
+#
+
+# Name of network namespace:
+NNSNAME=cafebabe
+
+# Base (e.g. VPN) host interface to link network namespace to:
+BASE_IF=vpn_tap1
+
+# Properties of virtual interface to create in network namespace:
+GUEST_ADDR=
+GUEST_PLEN=
+GUEST_BCAST=
+GUEST_MAC=FE:17:90:00:CA:FE
+
+# Properties of bridge interface to connect with network namespace:
+BRIDGE_IF=br0
+BRIDGE_ADDR=
+BRIDGE_PLEN=
+BRIDGE_BCAST=
+BRIDGE_MAC=FE:17:90:00:BA:BE
+
+# Space separated list of nameservers to provide DNS in the new network
+# namespace. If empty, the host resolv.conf will be copied:
+NAMESRV="8.8.8.8 8.8.4.4"
+
+# Program with optional arguments to run in the new network namespace:
+XCMD1="xterm"
+XCMD1_ARGS="-title $NNSNAME"
+
+# Virtual Ethernet to connect new network namespace to default namespace:
+VETH_HOST=veth_host
+VETH_GUEST=veth_guest
+