URI:
       ns: Spawn namespaced sandboxes - scripts - various script and utils
  HTML git clone git://z3bra.org/scripts
   DIR Log
   DIR Files
   DIR Refs
       ---
   DIR commit 1e86c2eaa87b6bfa7ae9c138d8c97ffc895f6990
   DIR parent 6ca2b5c87e6b9b8603995b3db4968f6e67f3b2f0
  HTML Author: Willy Goiffon <dev@z3bra.org>
       Date:   Thu, 20 Aug 2026 23:06:50 +0000
       
       ns: Spawn namespaced sandboxes
       
       Diffstat:
         A ns                                  |     261 +++++++++++++++++++++++++++++++
         A ns-install                          |     135 +++++++++++++++++++++++++++++++
         A ns-mount                            |      42 +++++++++++++++++++++++++++++++
       
       3 files changed, 438 insertions(+), 0 deletions(-)
       ---
   DIR diff --git a/ns b/ns
       @@ -0,0 +1,261 @@
       +#!/bin/sh
       +
       +set -e
       +
       +verbose=0
       +create=0
       +detach=0
       +nsdir=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ns
       +nstmpl="ns-XXXX"
       +nsuser="wgs"
       +nsnet="172.16.0.0/12"
       +
       +usage() {
       +        echo "usage: $(basename $0) [-vclk] [-n name] [cmd [arg…]]" >&2
       +}
       +
       +logset() {
       +        [ "${verbose:-0}" -gt 0 ] && printf '[ ] %s ' "$*" >&2 || true
       +}
       +
       +logchk() {
       +        [ "${verbose:-0}" -gt 0 ] && printf '\r[x]\n' >&2 || true
       +}
       +
       +logdie() {
       +        [ "${verbose:-0}" -gt 0 ] && printf '\r[!]\n' >&2
       +        ns_cleanup $nsname
       +        exit 1
       +}
       +
       +nslist() {
       +        [ -d /run/netns ] && ls -1 /run/netns
       +}
       +
       +nspid() { # 2nd PID is unshare(1)'s first child, aka PID 1
       +        ip netns pids $nsname|head -n2|tail -n1
       +}
       +
       +nscwd() {
       +        readlink -f /proc/$(nspid)/cwd
       +}
       +
       +nsip() {
       +        ns_exec ip -4 -o addr show "if"|awk '{print $4}'
       +}
       +
       +ns_cleanup() {
       +        nsname="$1"
       +        if=$(ip -o link show|awk "/netns $nsname/{print \$2}"|cut -d@ -f1)
       +        if [ -n "$if" ]; then
       +                net=$(ip -o -4 addr show dev "$if"|awk '{print $4}')
       +                doas iptables -t nat -D POSTROUTING -s "$net" -j MASQUERADE
       +        fi
       +        ip netns pids $nsname | xargs -r kill -9
       +        doas ip netns del $nsname
       +        doas umount -Rq $nsdir/$nsname
       +        rmdir "$nsdir/$nsname"
       +}
       +
       +ns_mkfs() {
       +        dir=$1
       +        logset "Create rootfs structure"
       +
       +        mkdir -p $dir
       +        doas mount -t tmpfs tmpfs "$dir"
       +        for d in bin etc lib home/$nsuser tmp mnt dev dev/pts dev/shm sys proc; do
       +                mkdir -p $dir/$d
       +        done
       +        for d in null zero full random urandom tty; do
       +            touch "$dir/dev/$d"
       +            doas mount --bind "/dev/$d" "$dir/dev/$d"
       +        done
       +        logchk
       +
       +        logset "Generate system files"
       +        cp -r /usr/share/terminfo $dir/etc/
       +        install -D /etc/ssl/certs/ca-certificates.crt $dir/etc/ssl/certs/ca-certificates.crt
       +        echo "$nsname" > $dir/etc/hostname
       +        echo "hosts: files dns" > $dir/etc/nsswitch.conf
       +        echo "127.0.0.1 localhost" > $dir/etc/hosts
       +        printf 'nameserver %s\n' $(resolvectl dns|cut -d: -f2)|sort|uniq > $dir/etc/resolv.conf
       +        printf "export PS1='(%s) $ '" $nsname > $dir/etc/profile
       +        printf "%s:x:0:0::/home/%s:/bin/sh" $nsuser $nsuser > $dir/etc/passwd
       +        printf "%s:*::0:::::" $nsuser > $dir/etc/shadow
       +        printf "world:x:0:%s\n" $nsuser > $dir/etc/group
       +        ln -s ../proc/mounts "$dir/etc/mtab"
       +        ln -sf pts/ptmx "$dir/dev/ptmx"
       +        ln -sf / "$dir/usr"
       +        logchk
       +
       +        cat <<-EOF > "$dir/etc/rc"
       +        #!/bin/sh
       +        /bin/mount -t sysfs sys /sys
       +        /bin/mount -t tmpfs tmpfs /dev/shm
       +        /bin/mount -t devpts devpts /dev/pts -o newinstance,ptmxmode=0666
       +        /bin/hostname $nsname
       +        EOF
       +        chmod +x $dir/etc/rc
       +
       +        logset "Compile /bin/init"
       +        cc -x c - -o $dir/bin/init <<-EOF
       +        #include <signal.h>
       +        #include <unistd.h>
       +        #include <sys/wait.h>
       +        int main() {
       +          sigset_t set; int signum;
       +          if (getpid()!=1) return 1;
       +          sigfillset(&set); sigprocmask(SIG_BLOCK, &set, 0); 
       +          if (fork()>0) while(!sigwait(&set, &signum)) wait(NULL);
       +          sigprocmask(SIG_UNBLOCK, &set, 0); setsid(); setpgid(0, 0);
       +          return execve("/etc/rc", (char *[]){"rc", 0}, (char *[]){0});
       +        }
       +        EOF
       +        logchk
       +}
       +
       +ns_install() {
       +        dir=$1
       +        logset "Boostrap utilities"
       +        # https://pubs.opengroup.org/onlinepubs/9799919799/idx/utilities.html
       +        exe="admin alias ar asa at awk basename batch bc bg c17 cal"
       +        exe="$exe cat cd cflow chgrp chmod chown cksum cmp comm command"
       +        exe="$exe compress cp crontab csplit ctags cut cxref date dd"
       +        exe="$exe delta df diff dirname du echo ed env ex expand expr"
       +        exe="$exe false fc fg file find fold fuser gencat get getconf"
       +        exe="$exe getopts gettext grep hash head iconv id ipcrm ipcs"
       +        exe="$exe jobs join kill lex link ln locale localedef logger"
       +        exe="$exe logname lp ls m4 mailx make man mesg mkdir mkfifo more"
       +        exe="$exe msgfmt mv newgrp ngettext nice nl nm nohup od paste"
       +        exe="$exe patch pathchk pax pr printf prs ps pwd read readlink"
       +        exe="$exe realpath renice rm rmdel rmdir sact sccs sed sh sleep"
       +        exe="$exe sort split strings strip stty tabs tail talk tee test"
       +        exe="$exe time timeout touch tput tr true tsort tty type ulimit"
       +        exe="$exe umask unalias uname uncompress unexpand unget uniq"
       +        exe="$exe unlink uucp uudecode uuencode uustat uux val vi wait"
       +        exe="$exe wc what who write xargs xgettext yacc zcat"
       +
       +        # Missing from the above list
       +        exe="$exe mksh bash busybox mount umount mountpoint"
       +        exe="$exe tar gzip bzip2 xz zstd less"
       +
       +        # Network utils
       +        exe="$exe hostname ip ping host dig curl ssh tcpdump"
       +
       +        which $exe|xargs cp -t $dir/bin
       +        ldd $(readlink -f `which $exe`) 2>/dev/null \
       +                |awk '/=>/{print $3}/^\s+\//{print $1}' \
       +                |sed 's,^/usr,,'|sort|uniq|cpio --quiet -updL $dir
       +        logchk
       +}
       +
       +ns_boot() {
       +        ns=$1
       +        logset "Start init process"
       +        unshare -rf -Uuimpn --mount-proc --kill-child -R $ns -- /bin/init &
       +        doas ip netns attach $nsname $!
       +
       +        [ -z "$(nspid)" ] && logdie "Init process failed to start"
       +        logchk
       +}
       +
       +ns_network() {
       +        ns=$1
       +        pid=$(nspid)
       +        nsif="$nsname"
       +        logset "Setup network"
       +
       +        # Grab a random prefix from the given netblock to handle
       +        # communication between host and namespace (through NAT)
       +        pfx=20
       +        net=$(ipcalc -nb "$nsnet" $pfx|awk '/Network/{print $2}'|shuf|head -n1)
       +        min=$(ipcalc -nb $net|awk '/HostMin/{print $2}')
       +        max=$(ipcalc -nb $net|awk '/HostMax/{print $2}')
       +        doas ip link add $nsif type veth peer ${nsif}-peer
       +        doas ip link set $nsif up
       +        doas ip addr add "$min/$pfx" dev $nsif
       +        doas sysctl -qw net.ipv4.ip_forward=1
       +        doas iptables -t nat -A POSTROUTING -s $net -j MASQUERADE
       +
       +        # Move 2nd pair of veth inside the namespace
       +        doas ip link set ${nsif}-peer netns $nsname
       +        nsenter -t "$(nspid)" -r --wd=$ns --preserve-credentials -U -u -p -i -n -m -- env -i /bin/sh <<-EOF
       +        /bin/ip link set lo up
       +        /bin/ip link set ${nsif}-peer name if
       +        /bin/ip link set if up
       +        /bin/ip address add ${max}/${pfx} dev if
       +        /bin/ip route add default via ${min}
       +        EOF
       +        logchk
       +}
       +
       +ns_pop() {
       +        # Generate random nsname if not provided
       +        : ${nsname:=ns-$(tr -cd a-z </dev/urandom|dd status=none bs=1 count=5)}
       +
       +        mkdir -p   "$nsdir/$nsname"
       +        trap "ns_cleanup $nsname" EXIT INT TERM QUIT
       +
       +        ns_mkfs    "$nsdir/$nsname"
       +        ns_install "$nsdir/$nsname"
       +        ns_boot    "$nsdir/$nsname"
       +        ns_network "$nsdir/$nsname"
       +        trap - EXIT
       +}
       +
       +ns_exec() {
       +        pid=$(nspid)
       +        cwd=$(nscwd)
       +        nshome=$(head -n1 $cwd/etc/passwd|cut -d: -f 6)
       +        nsenter -t $pid -r --wd=$cwd --preserve-credentials -U -u -p -i -n -m -- \
       +                env -i -C $nshome \
       +                        PATH=/bin \
       +                        USER=$nsuser \
       +                        HOME=$nshome \
       +                        TERM="${TERM:-vt100}" \
       +                        LESS="${LESS:-R}" \
       +                        PAGER="${PAGER:-less}" \
       +                        EDITOR="${EDITOR:-vi}" \
       +                        VISUAL="${VISUAL:-vi}" \
       +                        "$@"
       +}
       +
       +ns_info() {
       +        nsname="$1"
       +        [ -z "$(nspid)" ] && exit 1
       +        cat <<-EOF
       +        name: $nsname
       +        user: $(cut -d: -f1 < $(nscwd)/etc/passwd)
       +        root: $(nscwd)
       +        init: $(nspid)
       +        ipv4: $(nsip)
       +        EOF
       +}
       +
       +while getopts "hvcdli:k:n:u:" OPT; do
       +        case $OPT in
       +        d) detach=1;;
       +        n) nsname="$OPTARG";;
       +        u) nsuser="$OPTARG";;
       +        v) verbose=1;;
       +        c) create=1;; 
       +        i) ns_info "$OPTARG"; exit 0;;
       +        k) ns_cleanup "$OPTARG"; exit 0;;
       +        l) nslist; exit 0;;
       +        h) usage; exit 0;;
       +        *) usage; exit 1;;
       +        esac
       +done
       +shift $((OPTIND-1))
       +
       +# Calling ns(1) without argument will attach to the first available
       +# namespace. If no namespace is available, a new one will be spun up.
       +if [ "$create" -eq 1 -o -z "$(nslist)" ]; then
       +        ns_pop
       +        [ "$detach" -eq 1 ] && { echo $nsname; exit 0; }
       +fi
       +
       +[ -z "$nsname" ] && nsname=$(nslist|head -n1)
       +
       +# Pop a shell or whatever args provided
       +ns_exec ${@:-mksh -l}
   DIR diff --git a/ns-install b/ns-install
       @@ -0,0 +1,135 @@
       +#!/bin/sh
       +
       +usage() {
       +        echo "usage: $(basename $0) [-al] [-n ns] [pkg...]" >&2
       +}
       +
       +# Fix missing shared libs
       +shlibfix() {
       +        find $1/bin $1/libexec -executable -type f -exec ldd {} + 2>/dev/null \
       +                |awk '/=>/{print $3}/^\s+\//{print $1}' \
       +                |sed 's,/usr,,'|sort|uniq|cpio --quiet -updL $1
       +}
       +
       +# https://claude.ai/install.sh
       +install_claude() {
       +        d="$1"
       +        u="https://downloads.claude.ai/claude-code-releases"
       +        a="linux-x64"
       +        v=$(curl -sSfL "$u/latest")
       +        flock $l curl -sSfL "$u/$v/$a/claude" -o $root/bin/claude &
       +        spinner -p "Install claude ($v)" $l
       +        chmod +x $d/bin/claude
       +}
       +
       +# https://developer.hashicorp.com/terraform/install
       +install_terraform() {
       +        z=$(mktemp --suffix .zip)
       +        u="https://releases.hashicorp.com/terraform"
       +        a="linux_amd64"
       +        v=$(curl -s "https://developer.hashicorp.com/terraform/install"|sed -rn 's/.* ([0-9.]*) \(latest\).*/\1/p')
       +        flock $l curl -sSfL "$u/$v/terraform_${v}_${a}.zip" -o $z &
       +        spinner -p "Install terraform ($v)" $l
       +        unzip -q -o -d $root/bin $z terraform
       +        rm -f $z
       +}
       +
       +# https://gameoftrees.org/portable.html
       +install_got() {
       +        d="$1"
       +        tmp=$(mktemp -d)
       +        u="https://gameoftrees.org/releases/portable"
       +        v=$(curl -sSfL "https://gameoftrees.org/portable.html"|sed -rn '/Latest Release/s/.*>([0-9.]+).*/\1/p')
       +        flock $l sh <<-EOF &
       +        curl -sSfL "$u/got-portable-${v}.tar.gz" | tar -xz -C $tmp
       +        cd "$tmp/got-portable-${v}"
       +        ./configure -q CPPFLAGS='-DGOT_DIAL_PATH_SSH=\"/bin/ssh\"' --prefix='' >/dev/null
       +        make -j $(nproc) CC=clang DESTDIR="$d" install >/dev/null 2>&1
       +        EOF
       +        spinner -p "Install got (${v})" $l
       +        rm -rf $tmp
       +}
       +
       +# https://www.brain-dump.org/projects/abduco
       +install_abduco() {
       +        d="$1"
       +        tmp=$(mktemp -d)
       +        u="https://github.com/martanne/abduco/releases/download"
       +        v="0.6"
       +        flock $l sh <<-EOF &
       +        curl -sSfL "$u/v${v}/abduco-${v}.tar.gz" | tar -xz -C $tmp
       +        cd "$tmp/abduco-${v}"
       +        make -j $(nproc) CC=clang DESTDIR="$d" PREFIX= install >/dev/null 2>&1
       +        EOF
       +        spinner -p "Install abduco (${v})" $l
       +        rm -rf $tmp
       +}
       +
       +# https://github.com/martanne/vis
       +install_vis() {
       +        d="$1"
       +        tmp=$(mktemp -d)
       +        u="https://github.com/martanne/vis"
       +        a="https://api.github.com/repos/martanne/vis"
       +        v=$(curl -sSfL "$a/commits" 2>/dev/null|grep -m1 -Eo '"sha": "[0-9a-f]+"'|cut -b9-48)
       +        flock $l sh <<-EOF &
       +        curl -sSfL "$u/archive/${v}.tar.gz" | tar -xz -C $tmp
       +        cd "$tmp/vis-${v}"
       +        ./configure --enable-static >/dev/null
       +        make -j $(nproc) DESTDIR="$d" PREFIX= install >/dev/null 2>&1
       +        EOF
       +        sha=$(echo $v|cut -c 1-7)
       +        spinner -p "Install vis ($sha)" $l
       +        rm -rf $tmp
       +}
       +
       +install_nodejs() {
       +        d="$1"
       +        tmp=$(mktemp -d)
       +        v=
       +        u="https://nodejs.org/dist"
       +        v=$(curl -sSfL $u/latest/SHASUMS256.txt|sed -rn '/linux-x64.tar.gz/s/.*(v[0-9.]+).*/\1/p')
       +        flock $l sh <<-EOF &
       +        curl -sSfL "$u/$v/node-$v-linux-x64.tar.gz" | tar -xz -C $tmp
       +        rsync -a $tmp/node-${v}-linux-x64/bin $tmp/node-${v}-linux-x64/lib $root/
       +        EOF
       +        spinner -p "Install nodejs (${v})" $l
       +        rm -rf $tmp
       +}
       +
       +list_pkg() {
       +        sed -rn 's/^install_(.*)\(\) \{/\1/p' "$0"|sort
       +}
       +
       +pkg=
       +nsname=$(ns -l|head -n1)
       +while getopts "ahln:" OPT; do
       +        case $OPT in
       +        a) pkg=$(list_pkg);;
       +        l) list_pkg; exit 0;;
       +        n) nsname="$OPTARG";;
       +        h) usage; exit 0;;
       +        *) usage; exit 1;;
       +        esac
       +done
       +shift $((OPTIND-1))
       +
       +: ${pkg:=$@}
       +
       +if [ -z "$pkg" ] && [ $# -lt 1 ]; then
       +        usage
       +        exit 1
       +fi
       +
       +root=$(ns -i $nsname|awk '/^root:/{print $2}')
       +for p in $pkg; do
       +        if ! list_pkg|grep -q "^${p}\$"; then
       +                printf '%s: Not in package list\n' "$p" >&2
       +                exit 1
       +        fi
       +done
       +
       +l=$(mktemp)
       +trap "rm -f $l" EXIT INT TERM QUIT
       +for p in $pkg; do install_$p $root; done
       +shlibfix $root
   DIR diff --git a/ns-mount b/ns-mount
       @@ -0,0 +1,42 @@
       +#!/bin/sh
       +
       +ro=""
       +rw=""
       +
       +usage() {
       +        echo "usage: $(basename $0) -n name [-rw dir] mountpoint" >&2
       +}
       +
       +while getopts "hn:r:w:" OPT; do
       +        case $OPT in
       +        n) nsname="$OPTARG";;
       +        r) ro="$OPTARG";;
       +        w) rw="$OPTARG";;
       +        h) usage; exit 0;;
       +        *) usage; exit 1;;
       +        esac
       +done
       +shift $((OPTIND-1))
       +
       +if [ -z "$nsname" ] || [ $# -lt 1 ]; then
       +        usage
       +        exit 1
       +fi
       +
       +mp="$1"
       +ns=$(ns -i $nsname|awk '/root/{print $2}')
       +pid=$(ns -i $nsname|awk '/init/{print $2}')
       +
       +opt="lowerdir=${ns}/${mp}"
       +if [ -n "$ro" ]; then
       +        opt="${opt}:$(readlink -f $ro)"
       +elif [ -n "$rw" ]; then
       +        wd="$HOME/.cache/overlayfs/work/$nsname"
       +        mkdir -p "$wd" "${ns}/${mp}"
       +        opt="${opt},workdir=$wd"
       +        opt="${opt},upperdir=$(readlink -f $rw)"
       +fi
       +
       +mkdir -p "${ns}/${mp}"
       +nsenter -t $pid -m -U --preserve-credentials -- \
       +        mount --onlyonce -t overlay overlay -o "$opt" "$ns/$mp"