fix regression
[newns] / newns
diff --git a/newns b/newns
index 894ac310b2963866328fe545b20ced78e1d2ceab..9a7a6ff10fff25a11056d681fb4ad70826b77b2e 100755 (executable)
--- a/newns
+++ b/newns
@@ -1,5 +1,12 @@
 #!/bin/bash
-# Copyright (C) 2017 Ian Kelling
+# I, Ian Kelling, follow the GNU license recommendations at
+# https://www.gnu.org/licenses/license-recommendations.en.html. They
+# recommend that small programs, < 300 lines, be licensed under the
+# Apache License 2.0. This file contains or is part of one or more small
+# programs. If a small program grows beyond 300 lines, I plan to switch
+# its license to GPL.
+
+# Copyright 2024 Ian Kelling
 
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 
 [[ $EUID == 0 ]] || exec sudo -E "$BASH_SOURCE" "$@"
 
-tmp="$(readlink -f "${BASH_SOURCE}")"; script_dir="${tmp%/*}"
-if [[ ! $ERRHANDLE_PATH ]]; then
-  ERRHANDLE_PATH="$script_dir"/../errhandle/err
-fi
-if [[ -s $ERRHANDLE_PATH ]]; then
-  source $ERRHANDLE_PATH
-else
-  cd "$script_dir"
-  if ! wget -O err 'https://iankelling.org/git/?p=errhandle;a=blob_plain;f=err;hb=HEAD'; then
-    echo "$0: failed to get errhandle dependency" >&2
-    exit 1
+# https://savannah.nongnu.org/projects/bash-bear-trap/
+set -e; . /usr/local/lib/bash-bear; set +e
+
+m() {
+  local out
+  printf "newns: %s\n" "$*"
+  if ! out=$("$@" 2>&1); then
+    echo "newns: WARNING: last command exit code: $?"
+  elif [[ ! $out ]]; then
+    echo "newns: WARNING: no output from last command"
   fi
-  source err
-fi
+}
 
 usage() {
   cat <<EOF
-usage: ${0##*/} [OPTS] start|stop NS_NAME
+usage: ${0##*/} [OPTS] start|stop|show NS_NAME
 Nat a network namespace. systemd friendly
 
 Also creates a mount namespace with a cloned /run/resolvconf.
 
+Arguments:
+
+start|stop: these do what they say.
+
+show: Show the state we expected to be there or not there based on
+start/stop. This is useful for debugging.
+
+NS_NAME: We use this to name the interfaces we create, the mount
+namespace, and if we are creating a named network space, that too.
+
 -c, --create    Create or destroy a named network namespace. When running from
                 the same network namespace as pid 1, this is set automatically.
-                A systemd created private network is in a network namespace
-                different than pid 1.
+                A systemd created private network is in an unnamed network namespace
+                different than pid 1. I haven't found a need for a named network
+                namespace in that case.
 -n NETWORK      x.x.x /24 private network to use. If not specified, uses
                 the first unused one starting at 10.173.1
 -h, --help      Show this help and exit.
@@ -49,7 +65,7 @@ Also creates a mount namespace with a cloned /run/resolvconf.
 From a normal shell:
 
 If we do create the netns, to join it with a shell, we can do (as root)
-/usr/bin/nsenter --mount=/root/mount_namespaces/NAME --net=/var/run/netns/NAME bash
+/usr/bin/nsenter --mount=/run/mount-namespaces/NAME --net=/var/run/netns/NAME bash
 
 If you dont care about the mount namespace, you can leave that option off.
 
@@ -61,10 +77,10 @@ would be called from ExecStartPre, and or subsequent units called with
 JoinsNamespaceOf= and PrivateNetwork=true.
 
 If resolvconf is installed, we create a named mount namespace under
-/root/mount_namespaces, so we can alter some system config for this
+/run/mount-namespaces, so we can alter some system config for this
 namespace. systemd command lines would be prefixed with:
 
-/usr/bin/nsenter --mount=/root/mount_namespaces/NS_NAME
+/usr/bin/nsenter --mount=/run/mount-namespaces/NS_NAME
 
 Note, this means that they can't run as unpriveledged users, but once
 systemd 233 comes out, it will have a bind mount option from within unit
@@ -72,13 +88,9 @@ files, so the mount namespace won't be needed for most use cases, and I
 will update the script to that the mount namespace not created unless a
 flag is passed in. Patch welcome to add that flag before then.
 
-This script has a dependency which you can download manually or it
-will be automatically downloaded into the same directory.
-It handles errors by printing stack trace and and cleaning up the namespaces.
-To download manually,
-git clone https://iankelling.org/git/errhandle
-into an adjacent directory, or
-export ERRHANDLE_PATH to point to the 'err' file in that repo.
+
+This script has a dependency
+https://savannah.nongnu.org/projects/bash-bear-trap/ . Search the script for "source" to see where to install or modify the  installed location.
 
 
 Background on this project (you can skip if you like):
@@ -100,6 +112,8 @@ copy /run/resolvconf somehwere then bind mount it on top of
 
 Note: for debugging, adding set -x is a pretty good option.
 
+TODO: make "start" be idempotent.
+
 Please email me if you have a patches, bugs, feedback, or republish this
 somewhere else: Ian Kelling <ian@iankelling.org>.
 EOF
@@ -109,7 +123,7 @@ EOF
 
 #### begin arg parsing ####
 create=false
-temp=$(getopt -l help,create hcn: "$@") || usage 1
+temp=$(getopt -l help,create hcdn: "$@") || usage 1
 eval set -- "$temp"
 while true; do
   case $1 in
@@ -147,54 +161,43 @@ v0=veth0-$nn
 v1=veth1-$nn
 ip_base=10.173
 
-if ! $create && [[ $(readlink /proc/self/ns/net) == "$(readlink /proc/1/ns/net)" ]]; then
-  create=true
-fi
-
-# make the default network namespace be named
 
+### begin make the default network namespace be named "default" ###
 mkdir -p /run/netns
 target=/run/netns/default
 if [[ ! -e $target && ! -L $target ]]; then
   # -f to avoid a race condition with running twice
   ln -sf /proc/1/ns/net $target
 fi
+### end make the default network namespace be named "default" ###
 
-ipd() { ip -n default "$@"; }
-
-
+if ! $create && [[ $(readlink /proc/self/ns/net) == "$(readlink /proc/1/ns/net)" ]]; then
+  create=true
+fi
 # otherwise we are already in the network namespace and it's unnamed.
 if $create; then
   ipnnargs="-n $nn"
 fi
+
+
+ipd() { ip -n default "$@"; }
+
 # run ip in the network namespace
 ipnn() { ip $ipnnargs "$@"; }
 
 # default network namespace exec
 dexec() { ip netns exec default "$@"; }
 # mount namespace exec
-mexec() { /usr/bin/nsenter --mount=/root/mount_namespaces/$nn "$@"; }
-
+mexec() { /usr/bin/nsenter --mount=/run/mount-namespaces/$nn "$@"; }
 
-# background: head -n1 is defensive. Not sure if there is some weird feature
-# for 2 routes to be 0/0.
-gateway_ifs=($(ipd route list exact 0/0 | head -n1| sed -r 's/.*dev\s+(\S+).*/\1/'))
-
-if [[ ! $gateway_ifs ]]; then
-  cat >&2 <<EOF
-$0: error: failed to find gateway interface. No output from:
-ipd route list exact 0/0 | head -n1| sed -r 's/.*dev\s+(\S+).*/\1/'
-output from "ipd route list exact 0/0":
-$(ipd route list exact 0/0)
-EOF
-  exit 1
-fi
 
 nat() {
-  for if in ${gateway_ifs[@]}; do
-    dexec iptables -t nat $1 POSTROUTING -s $network.0/24 -o $if -j MASQUERADE \
-          -m comment --comment "systemd network namespace nat"
-  done
+  # Note: duplicated in show()
+  # Note, in a previous commit i specified the output interface with -o,
+  # but that broke things when my gateway interface changed, and I can't
+  # see any advantage to it, so I removed it.
+  dexec iptables -t nat $1 POSTROUTING -s $network.0/24 -j MASQUERADE \
+        -m comment --comment "systemd network namespace nat"
 }
 
 # d = default
@@ -205,44 +208,53 @@ diptables-add() {
 
 }
 
-find_network() {
+find-network() {
   if [[ $network ]]; then
     return
   fi
   found=false
-  existing=false
   ips="$(ipd addr show | awk '$1 == "inet" {print $2}')"
   for ((i=1; i <= 254; i++)); do
     network=$ip_base.$i
-    if printf "%s\n" "$ips" | grep "^${network//./\\.}" >/dev/null; then
-      existing=true
-    else
+    if ! printf "%s\n" "$ips" | grep "^${network//./\\.}" >/dev/null; then
       found=true
       break
     fi
   done
-}
-
-start() {
-  find_network
   if ! $found; then
     echo "$0: error: no open network found"
     exit 1
   fi
+}
+
+# ip add idempotent (if it doesn't exist already)
+ip-add() {
+  local cmd net dev
+  cmd=$1
+  net=$2
+  dev=$3
+  if ! $cmd addr show dev $dev | sed 's/^ *//;s/ *$//' | grep -xF "inet $net scope global $dev"; then
+    $cmd addr add $net dev $dev
+  fi
+
+}
+
+start() {
+  find-network
 
   #### begin mount namespace setup ####
-  mkdir -p /root/mount_namespaces
-  if ! mountpoint /root/mount_namespaces >/dev/null; then
-    mount --bind /root/mount_namespaces /root/mount_namespaces
+  mkdir -p /run/mount-namespaces
+  if ! mountpoint /run/mount-namespaces >/dev/null; then
+    mount --bind /run/mount-namespaces /run/mount-namespaces
   fi
   # note: This is outside the mount condition because I've mysteriously
   # had this become shared instead of private, perhaps it
   # got remounted somehow and lost the setting.
-  mount --make-private /root/mount_namespaces
-  if [[ ! -e /root/mount_namespaces/$nn ]]; then
-    touch /root/mount_namespaces/$nn
+  mount --make-private /run/mount-namespaces
+  if [[ ! -e /run/mount-namespaces/$nn ]]; then
+    touch /run/mount-namespaces/$nn
   fi
-  if ! mountpoint /root/mount_namespaces/$nn >/dev/null; then
+  if ! mountpoint /run/mount-namespaces/$nn >/dev/null; then
     # Here, we specify that we only want mount changes changes under
     # this mountpoint to be propagated into the bind, but changes
     # from within the bind do not propagate to outside the bind.
@@ -252,23 +264,25 @@ start() {
     # documentation on propagation is a bit weird because it
     # confusingly talks about binds, namespaces, and mirrors (which
     # seems to be just another name for bind), shared subtrees
-    # (which seems to a term for binds and namespaces), and does not
+    # (which seems to be a term for binds and namespaces), and does not
     # properly specify whether the documentation applies to binds,
     # namespaces, or both. Notably, propagation for binds is marked
     # on the original mount point, and propagation for a mount
     # namespace is marked on mounts within the namespace.
-    unshare --propagation slave --mount=/root/mount_namespaces/$nn /bin/true
+    unshare --propagation slave --mount=/run/mount-namespaces/$nn /bin/true
   fi
 
   ####   end mount namespace setup ####
 
 
   if $create; then
-    ip netns add $nn
+    if ! ip netns | grep -xF $nn &>/dev/null; then
+      ip netns add $nn
+    fi
     ip -n $nn link set dev lo up
   fi
 
-  echo 1 | dexec dd of=/proc/sys/net/ipv4/ip_forward 2>/dev/null
+  echo 1 | dexec dd of=/proc/sys/net/ipv4/ip_forward status=none
 
   # docker helpfully changes the default FORWARD to drop...
   diptables-add FORWARD -i $v0 -j ACCEPT
@@ -278,12 +292,39 @@ start() {
   err-cleanup() { stop; }
   ipnn link add $v0 type veth peer name $v1
   ipnn link set $v0 netns default
-  ipd addr add $network.1/24 dev $v0
+  ip-add ipd $network.1/24 $v0
   ipd link set $v0 up
   nat -C &>/dev/null || nat -A
-  ipnn addr add $network.2/24 dev $v1
+  ip-add ipnn $network.2/24 $v1
   ipnn link set $v1 up
-  ipnn route add default via $network.1
+  cmd="ipnn route add default via $network.1"
+  $cmd
+  fails=0
+  max_fails=2
+  # I've had adding the default route mysteriously fail on boot, so
+  # here we check that it succeeded, do a sleep and a retry.
+  while true; do
+    default_route=$(ipnn route show default | sed -r 's,^[[:space:]]+|[[:space:]]+$,,')
+    if [[ $default_route != "default via $network.1 dev $v1" ]]; then
+      fails=$((fails + 1))
+    else
+      break
+    fi
+    if (( fails >= max_fails )); then
+      echo "$0: ERROR: default route added but not found, retried $max_fails. expected route: 'default via $network.1 dev $v1', found: '$default_route'"
+      # Note: for debugging, if you have a systemd unit which tears down
+      # the newns upon failure, you may want to uncomment the break so
+      # that we proceed and can inspect the system.  break
+      exit 1
+    else
+      sleep 1
+      $cmd
+    fi
+  done
+  if (( fails >= 1 )); then
+    echo "$0: WARNING: route added but not found until retried $max_fails times: $cmd"
+  fi
+
 
   ###### begin setup resolvconf
   if [[ -e /run/resolvconf ]]; then # resolvconf probably installed
@@ -319,19 +360,17 @@ start() {
 
   fi # end if [[ -e /run/resolvconf ]]
   ######  end setup resolvconf
-
-
 }
 
 stop() {
+  if [[ ! $network ]]; then
+    network=$(ipd -f inet a show dev $v0 2>/dev/null | awk '/inet / {print $2}' | sed -r 's,\.[0-9]+/.*,,' ||:)
+  fi
   if ipd link list $v0 &>/dev/null; then
     # this also deletes $v1 and the route we added.
     ipd link del $v0
   fi
-  find_network
-  if ! $existing; then
-    if nat -C &>/dev/null; then nat -D; fi
-  fi
+  if [[ $network ]] && nat -C &>/dev/null; then nat -D; fi
   dexec iptables -D FORWARD -i $v0 -j ACCEPT &>/dev/null ||:
   if $create && [[ -e /var/run/netns/$nn ]]; then
     ip netns del $nn
@@ -342,13 +381,22 @@ stop() {
     mexec umount /run/resolvconf
   fi
 
-  if mountpoint /root/mount_namespaces/$nn >/dev/null; then
-    umount /root/mount_namespaces/$nn
+  if mountpoint /run/mount-namespaces/$nn >/dev/null; then
+    umount /run/mount-namespaces/$nn
   fi
 }
 
+show() {
+  m ipd link list $v0
+  m dexec iptables -t nat -C POSTROUTING -s $network.0/24 -j MASQUERADE \
+    -m comment --comment "systemd network namespace nat" ||:
+  m dexec iptables -C FORWARD -i $v0 -j ACCEPT
+  m mexec mountpoint /run/resolvconf
+  m mountpoint /run/mount-namespaces/$nn
+}
+
 case $action in
-  start|stop)
+  start|stop|show)
     $action
     ;;
   *)