#!/usr/bin/env bash # Validate a VShell container rootfs without a container runtime. # # Runs the vendor binaries, checks the configuration, then starts the image's # real entrypoint (docker/entrypoint.sh) inside a user-namespace chroot on # high ports and probes every listener from the outside. # # Usage: tools/validate.sh [/tmp/vshell-rootfs] # # Exit status 0 means every check passed. set -uo pipefail ROOTFS=${1:-/tmp/vshell-rootfs} CFG=/tmp/validate_config CERT=/tmp/validate.p12 SSH_PORT=2222 FTPS_PORT=2121 FTPSI_PORT=2990 HTTP_PORT=8080 HTTPS_PORT=8443 FAILED=0 say() { printf '%s\n' "$*"; } fail() { printf 'FAIL: %s\n' "$*"; FAILED=1; } [ -x "$ROOTFS/usr/local/sbin/vshelld" ] || { say "no VShell install under $ROOTFS"; exit 1; } rm -f "$ROOTFS$CFG" "$ROOTFS$CERT" # The chroot needs /dev entries for the daemons and openssl to behave normally. in_ns() { unshare -rm sh -c " for d in null zero random urandom tty; do [ -e $ROOTFS/dev/\$d ] || touch $ROOTFS/dev/\$d mount --bind /dev/\$d $ROOTFS/dev/\$d done $1 " } say '== binaries, configuration and license' in_ns "chroot $ROOTFS /bin/bash -c ' set -e /usr/local/sbin/vshelld -version /usr/local/sbin/vshell-httpsd -version /usr/local/sbin/vshell-ftpsd -version /usr/local/sbin/vshelld -test_config -f /usr/local/etc/vshelld_config /usr/local/sbin/vshelld -license '" || fail 'version/config/license checks' say '== starting services through the image entrypoint' in_ns "chroot $ROOTFS /bin/bash -c ' exec env VSHELL_CONFIG=$CFG VSHELL_TLS_P12=$CERT \ VSHELL_SSH_ADDR=0.0.0.0:$SSH_PORT VSHELL_HTTPS_ADDR=0.0.0.0:$HTTPS_PORT \ VSHELL_HTTP_ADDR=0.0.0.0:$HTTP_PORT VSHELL_FTPS_EXPLICIT_ADDR=0.0.0.0:$FTPS_PORT \ VSHELL_FTPS_IMPLICIT_ADDR=0.0.0.0:$FTPSI_PORT \ /usr/local/bin/vshell-entrypoint '" >/tmp/vshell-validate.log 2>&1 & sleep 8 sed 's/^/ entrypoint: /' /tmp/vshell-validate.log banner() { # port, label local port=$1 label=$2 line if ! exec 3<>"/dev/tcp/127.0.0.1/$port" 2>/dev/null; then fail "$label ($port): connection refused"; return; fi if read -t 3 -r line <&3; then say "ok $label ($port): $line"; else fail "$label ($port): no banner"; fi exec 3<&- 3>&- } banner "$SSH_PORT" 'SSH ' banner "$FTPS_PORT" 'FTPS ' http_probe() { local port=$1 line if ! exec 3<>"/dev/tcp/127.0.0.1/$port" 2>/dev/null; then fail "HTTP ($port): connection refused"; return; fi printf 'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 if read -t 4 -r line <&3; then say "ok HTTP ($port): $line"; else fail "HTTP ($port): no response"; fi exec 3<&- 3>&- } http_probe "$HTTP_PORT" if command -v node >/dev/null; then node -e ' const tls = require("tls"); const port = Number(process.argv[1]); const s = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => { const c = s.getPeerCertificate(); console.log(`ok TLS (${port}): ${s.getProtocol()} CN=${c.subject && c.subject.CN}`); s.destroy(); }); s.on("error", (e) => { console.log(`FAIL: TLS (${port}): ${e.message}`); process.exitCode = 1; }); s.setTimeout(6000, () => { console.log(`FAIL: TLS (${port}): timeout`); s.destroy(); process.exitCode = 1; }); ' "$HTTPS_PORT" || FAILED=1 else say 'skip TLS probe (node not available)' fi say '== stopping' for p in $(ls /proc | grep -E '^[0-9]+$'); do [ -r "/proc/$p/cmdline" ] || continue c=$(tr '\0' ' ' <"/proc/$p/cmdline" 2>/dev/null) || continue case "$c" in /usr/local/sbin/*) kill "$p" 2>/dev/null && say " killed pid $p";; esac done wait 2>/dev/null if [ "$FAILED" -eq 0 ]; then say '== ALL CHECKS PASSED'; else say '== VALIDATION FAILED'; fi exit "$FAILED"