Skip to content

Instantly share code, notes, and snippets.

@CrackerJackMack
Created June 4, 2017 05:33
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CrackerJackMack/a620e9557bf6e015df540aa4e26510ff to your computer and use it in GitHub Desktop.
Save CrackerJackMack/a620e9557bf6e015df540aa4e26510ff to your computer and use it in GitHub Desktop.
systemd # workaround

This is my take on a systemd#2741 work-around. The lack of native namespace joining makes it so you have to do a dance with systemd's version of private namespaces and iproute2's version. The portal_network.sh is my rendition of iproute2.
Credit to ian-kelling's code and comment in the systemd issue that inspired me to research this further. Looking at iproute2's source the bash script should be identical, including the private mount and bind mounts. This level of compatibility gives the administrator the ability to use ip -n portal ... for any subsequent commands.

Files are dash separated due to this being a gist (slashes aren't allowed). PLace the files in the correct locations then run the following:

  1. systemctl enable portal.target portal.network.service whatever.service
  2. systemctl restart systemd-networkd # if using the vlan files and such
  3. systemctl start portal.target # will start all dependent services

Items to note

  • You can't assign an IP to an interface you move into a namespace. The IP address must be assigned after the interface is in the new namespace.
  • These example service files are easily templated with some minor changes. It was left out purely for simplicity sake.
  • The private namespace must be created by systemd. A namespace can't be created without a PID and you can't mount/join a namespace without a PID.
  • The VLAN's are purely as an example to include as an interface that won't take your whole systemd offline :)
[NetDev]
Name=vlan100
Kind=vlan
[VLAN]
Id=100
[Match]
Name=vlan100
[Network]
LLDP=no
LinkLocalAddressing=no
IPv6AcceptRA=no
[Unit]
Wants=network-online.target
After=network-online.target
[Unit]
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/portal_network.sh start
ExecStop=/usr/local/sbin/portal_network.sh stop
User=root
Group=root
PrivateNetwork=yes
MountFlags=private
[Install]
WantedBy=portal.target
[Unit]
Requires=portal.network.service
After=portal.network.service
JoinsNamespaceOf=portal.network.service
[Service]
ExecStart=/usr/local/bin/portal-capture
PrivateNetwork=yes
[Install]
WantedBy=portal.target
#!/bin/bash
PREFIX=/run/netns
ACTION=${1}
VLAN=100
SELF=$$
log() {
# log(): print and log all options to /tmp/portal/$VLAN.log
echo "[PRE] $@" | tee -a /tmp/portal.$VLAN.log
}
fail() {
log $@
exit 1
}
default_exec() {
# default_exec(): execute commands in the default namespace
# there is a nsenter from pypi, we won't want that one
/usr/bin/nsenter --net --mount -t 1 -- $@
}
ipn() {
# ipn(): execute /sbin/ip commands in a specific namespace
ns=$1
shift
/sbin/ip -n $ns $@
}
capture() {
# capture(): conditionally capture device from one namespace to another
# params:
# $1 => interface name
# $2 => source namespace name (default: `default`)
# $3 => destination namespace name or pid (default: $SELF)
intf=$1
src=${2:-default}
dest=${3:-$SELF}
# important: grep -w to match vlan226@ and not vlan2265@
ipn $dest -o link | grep -wq $intf
if [ $? -eq 0 ]; then
log "Device $intf exists in namespace, skipping capture"
return 0
fi
ipn $src -o link | grep -wq $intf
if [ $? -gt 0 ]; then
fail "BUG: Device $intf doesn't exist in source namespace: $src"
fi
log "Device $intf not found in namespace $dest, capturing from $src"
move_intf_netns $intf $src $dest
}
move_intf_netns() {
# move_intf_netns(): move interface from one namespace to another
# params:
# $1 => interface name
# $2 => source namespace name (default: `default`)
# $3 => destination namespace name or pid (default: $SELF)
intf=$1
src=${2:-default}
dest=${3:-$SELF}
log "moving $intf from $src to $3"
ipn $src link set $intf netns $dest || fail "failed to capture $intf"
}
ns_add() {
# ns_add(): add network namespace bind that iproute2 can manage
# params:
# $1 => network namespace name
# $2 => pid containing network namespace (default: $SELF)
nn=$PREFIX/$1
netns=${2:-$SELF}
log "trying to preserve namespace pid:$netns to $nn"
if ! mountpoint $PREFIX; then
fail "$PREFIX is not a mountpoint"
fi
# checking to see if this mount is the same as the namespace
# we want to bind. We don't want to clobber what's there as
# it is indicitive of a bug
if mountpoint $nn; then
current_ns=$(awk "\$5==\"$PREFIX\" { print \$4 }" /proc/$SELF/mountinfo)
target_ns=$(readlink /proc/$SELF/ns/net)
if [ "$current_ns" != "$target_ns"]; then
log "!!! attempted to add a namespace over an existing namespace"
fail "BUG: existing namespace $current_ns != $target_ns"
else
log $(default_exec mountpoint $nn)
return 0
fi
fi
log "attempting create bind mount $mnt"
touch $nn || fail "failed to touch $nn"
default_exec mount --bind /proc/$netns/ns/net $nn || fail "failed to bind /proc/$SELF/ns/net to $nn"
default_exec mount --make-slave $nn
log $(default_exec mountpoint $nn)
}
ns_rm() {
# ns_rm(): remove namespace
nn=$PREFIX/$1
if ! mountpoint $nn; then
log "namespace mount, $nn is not mounted"
default_exec rm -f $nn
fi
default_exec umount $nn || fail "failed to umount $nn"
default_exec rm -f $nn || fail "failed to remove $nn"
}
setup_default_namespace() {
# setup_default_namespace(): add network namespace called `default` that iproute2 can manage
# params: none
if [[ ! -e $PREFIX/default ]]; then
log "Missing default netns, adding"
ns_add "default" 1
fi
}
# main()
interface="vlan${VLAN}"
current_namespace=$(readlink /proc/$SELF/ns/net)
default_namespace=$(readlink /proc/1/ns/net)
netns="portal"
log "Current namespace: $current_namespace"
log "Default namespace: $default_namespace"
if [ "$current_namespace" == "$default_namespace" ]; then
# service definitions are incorrect!
fail "BUG: Running in parent namespace"
fi
if [[ $EUID -ne 0 ]]; then
fail "BUG: not running as root"
fi
case $ACTION in
start)
# create namespace bind so it's preserved
ns_add $netns
capture $interface default $netns
;;
stop)
capture $interface $netns default
ns_rm $netns
;;
*)
fail "unknown action: $ACTION"
;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment