#! /bin/sh # Copyright (c) 2023-2026 Slawomir Wojciech Wojtczak (vermaden) # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that following conditions are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS 'AS IS' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ------------------------------ # LIST AND MGMT FreeBSD jail(8) # ------------------------------ # vermaden [AT] interia [DOT] pl # https://vermaden.wordpress.com # DISPLAY USAGE INFORMATION __usage() { local NAME=${0##*/} echo "usage:" echo " ${NAME} list jail(8) containers" echo " ${NAME} -a list jail(8) containers with all IP(s)" echo " ${NAME} -d INTERVAL run '${NAME}' every INTERVAL seconds like top(1)" echo " ${NAME} -h show help" echo " ${NAME} --help show help" echo " ${NAME} help show help" echo " ${NAME} version show version" echo echo "manage:" echo " ${NAME} start" echo " ${NAME} restart" echo " ${NAME} stop" echo " ${NAME} status" echo " ${NAME} console" echo " ${NAME} shell" echo " ${NAME} jexec" echo echo "shorts:" echo " ${NAME} u UP ------> alias for start" echo " ${NAME} d DOWN ----> alias for stop" echo " ${NAME} r RESTART -> alias for restart" echo " ${NAME} c CONSOLE -> alias for console|shell|jexec" echo " ${NAME} s STATUS --> alias for status" echo exit 1 } # DISPLAY ONLY 3 FIRST INTERFACES __interfaces_limit() { if [ $( echo "${IFACE}" | wc -l | tr -d ' ' ) -gt 3 ] then IFACE=$( echo "${IFACE}" | head -3 | tr '\n' ' ' | tr ' ' '/' ) IFACE="${IFACE}(...)" else IFACE=$( echo "${IFACE}" | tr '\n' '/' | sed '$s/.$//' ) fi } __buffer() { # GET ALL OUTPUT INTO VARIABLE FOR FAST DISPLAY BUFFER="INFO: running '${NAME}' in INTERVAL mode every '${1}' seconds EXIT: press and hold [CTRL]+[C] keys to end $( ${0} )" echo "${BUFFER}" BACK=$( echo "${BUFFER}" | wc -l ) # MOVE CURSOR BACK TO BEGINING INSTEAD clear(1) BUILTIN seq ${BACK} | xargs -I- tput cuu1 } # INTERVAL RUN if [ "${1}" = "-d" ] then # CHECK IF INTERVAL IS NATURAL NUMBER REGEX_NUMBER=$( echo ${2} | grep -E -o "[0-9]+" ) if [ "${2}" != "${REGEX_NUMBER}" ] then echo "NOPE: the INTERVAL must be natural number" exit 1 fi # UNHIDE CURSOR ON EXIT trap 'tput cnorm' SIGINT SIGQUIT SIGHUP SIGTRAP SIGABRT SIGTERM # HIDE CURSOR tput civis # CLEAR SCREEN ONE TIME clear # GET basename(1) NAME OF PROGRAM NAME=${0##*/} # RUN 'INTERVAL' LOOP __buffer ${2} while sleep ${2} do __buffer ${2} done exit 0 fi # CHECK USER WITH whoami(1) if [ "$( whoami )" = "root" ] then : # CONTINUE AS 'root' USER else # CHECK doas(1) AND/OR sudo(8) EXISTENCE unalias doas unalias sudo if which doas 1> /dev/null 2> /dev/null then # CONTINUE USING doas(1) DOAS=doas elif which sudo 1> /dev/null 2> /dev/null then # CONTINUE USING sudo(8) DOAS=sudo else echo "NOPE: you need to be 'root' to use jmore(8) tool" exit 1 fi fi # CHECK PASSED ARGUMENTS case ${#} in (0) # DO NOTHING AND JUST DISPLAY LIST OF JAILS : ;; (1) # DISPLAY VERSION if [ "${1}" = "--version" -o \ "${1}" = "-version" -o \ "${1}" = "version" ] then echo " __ ____ __" echo " ___ / // \\\\ \\" echo " /__/__ __ _ ____ __ __ ____ / // / / \\ \\" echo " / // \\ / \\ / /_// _ \\/ / \ \\ / /" echo " / // / / // / // / / ___/\\ \\ / / // /" echo " __/ //__/__/__/ \\____//__/ \\____/ \\_\\\\____//_/" echo " /____/" echo echo "jmore(8) 0.5 2026/01/25" echo exit 0 fi # DISPLAY HELP IF NEEDED case ${1} in (-h|--h|help|-help|--help) __usage ;; esac # DO FULL LISTING WITH ALL INTERFACES AND IP ADDRESSES [ "${1}" = "-a" ] && FULL_LISTING=1 ;; (2) # MANAGE/OPERATE JAILS case ${2} in (u|start) ${DOAS} service jail onestart ${1} ;; (d|stop) ${DOAS} service jail onestop ${1} ;; (s|status) ${DOAS} service jail onestatus ${1} ;; (c|console|shell|jexec) if ! jls -j ${1} 1> /dev/null 2> /dev/null then echo "NOPE: jail '${1}' is not running" exit 1 fi env PS1="${1} # " PS2="> " ${DOAS} jexec ${1} ;; esac exit 0 ;; (*) # DISPLAY USAGE INFORMATION __usage ;; esac # SETTINGS AND INITIAL INFORMATION GATHERING JLS=$( jls 2> /dev/null ) NETSTAT=$( netstat -Win -f inet ) IFCONFIG=$( echo "${NETSTAT}" | sed 1d | awk '{print $1}' | uniq | tr '\n' ' ' ) # FIND AND SET bastille_prefix FOR BastilleBSD JAILS eval $( grep '^[^#]' /usr/local/etc/bastille/bastille.conf 2> /dev/null \ | grep -m 1 bastille_prefix \ | awk '{print $1}' ) if [ "${bastille_prefix}" != "" ] then BAST_DIR="${bastille_prefix}" unset bastille_prefix fi # GET LIST OF JAILS NAMES JAILS=$( grep -h '^[^#]' \ /etc/jail.conf \ /etc/jail.conf.d/*.conf \ "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null \ | grep -h -E '[[:space:]]*[[:alnum:]][[:space:]]*\{' \ | tr -d '\ \t{' ) # MAYBE THERE ARE NO JAILS AT ALL if [ "${JAILS}" = "" ] then echo "No configured Jails detected." exit 0 fi # OUTPUT START ( # PRINT HEADER echo JAIL JID "CPU/%" "RAM/mb" TYPE VER DIR IFACE "IP(s)" # PARSE JAILS INFORMATION while read JAIL do CONFIG=$( ( cat /etc/jail.conf 2> /dev/null cat /etc/jail.conf.d/*.conf 2> /dev/null cat "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null ) | grep '^[^#]' \ | grep -A 512 -E "[[:space:]]*${JAIL}*[[:space:]]*\{" \ | grep -B 512 -m 1 ".*[[:space:]]*\}[[:space:]]*$" ) # FIND DIR FOR RUNNING JAIL DIR=$( echo "${JLS}" | awk '{print $NF}' | grep -E -e "/${JAIL}$" -e "/${JAIL}/root" ) # FIND DIR FOR STOPPED JAIL if [ "${DIR}" = "" ] then DIR=$( grep -h '^[^#]' \ /etc/jail.conf \ /etc/jail.conf.d/*.conf \ "${BAST_DIR}"/jails/*/jail.conf 2> /dev/null \ | grep -A 512 -h -E "${JAIL}[[:space:]]\{" \ | grep -m 1 path \ | awk '{print $NF}' \ | tr -d ';' \ | sed -e "s.\${name}.${JAIL}.g" \ -e "s.\$name.${JAIL}.g" \ | tr -d '"' ) fi # FIND JAIL VERSION VER=$( ${DIR}/bin/freebsd-version -u 2> /dev/null \ | sed -e s.RELEASE.R.g \ -e s.CURRENT.C.g \ -e s.STABLE.S.g \ -e s.BETA.B.g ) # IF DIR IS ABSENT THEN SET IT TO [GONE] if [ ! -d "${DIR}" ] then DIR="[GONE]${DIR}" fi NETSTAT_JAIL=$( ${DOAS} jexec ${JAIL} netstat -Win -f inet 2> /dev/null ) # FIND JAIL IP(s) IPS=$( echo "${NETSTAT_JAIL}" \ | sed 1d \ | awk '{print $4}' \ | grep -v '127.0.0.1' \ | tr '\n' '/' \ | sed '$s/.$//' ) # FIND JAIL TYPE TYPE=$( ${DOAS} jexec ${JAIL} sysctl -n security.jail.vnet 2> /dev/null ) case ${TYPE} in # VNET JAIL (1) TYPE=vnet IFACE=$( echo "${NETSTAT_JAIL}" | sed 1d | grep -v '127.0.0.0/8' | awk '{print $1}' ) case ${FULL_LISTING} in (1) IFACE=$( echo "${IFACE}" | tr '\n' '/' | sed '$s/.$//' ) ;; (*) IPS=$( echo "${IPS}" | tr '/' '\n' | head -3 | tr '\n' '/' | sed '$s/.$//' ) __interfaces_limit ;; esac ;; # CLASSIC JAIL (0) TYPE=std while read IP do while read INTERFACE do if echo "${NETSTAT}" | grep -v '127.0.0.0/8' | grep -q -E -- "${IP}/" then IFACE=${INTERFACE} break fi done << EOF_INTERFACE $( echo "${IFCONFIG}" | tr ' ' '\n' ) EOF_INTERFACE done << EOF_IP $( echo "${IPS}" | tr '/' '\n' ) EOF_IP ;; # MORE DETECTION NEEDED (*) # MORE VNET JAIL if echo "${CONFIG}" | grep -q 'vnet.interface' then TYPE=vnet IFACE=$( echo "${CONFIG}" | grep 'vnet.interface' | awk '{print $NF}' | tr -d ';"' | tr ',' '\n' ) __interfaces_limit else # MORE CLASSIC JAIL TYPE=std IFTYPE=$( echo "${CONFIG}" | grep 'ip4.addr' ) case ${IFTYPE} in # PART FOR THIS FORMAT BELOW # ip4.addr = em0|10.0.0.1; (*\|*) IFACE=$( echo "${IFTYPE}" | awk -F '=' '{print $2}' ) IPS=$( echo "${IFACE}" | awk -F '|' '{print $2}' | tr -c -d '0-9.' ) IFACE=$( echo "${IFACE}" | awk -F '|' '{print $1}' ) ;; # PART FOR THIS FORMAT BELOW # interface = em0; # ip4.addr = 10.0.0.1; (*) IFACE=$( echo "${CONFIG}" | grep 'interface' | awk '{print $NF}' | tr -d ';' | tr ',' '\n' ) __interfaces_limit IPS=$( echo "${CONFIG}" | grep '\.addr' | awk '{print $NF}' | tr -d ';' ) ;; esac fi ;; esac # GET JAIL JID JID=$( echo "${JLS}" | grep -E -e "/${JAIL}$" -e "/${JAIL}/root" | awk '{print $1}' ) # IF JAIL IS NOT RUNNING THEN SET JID TO '-' VALUE if [ "${JID}" = "" ] then if ! ${DOAS} jexec ${JAIL} echo / 1> /dev/null 2> /dev/null then JID=- fi fi # CALCULATE CPU/RAM USAGE RAM=0 CPU=0 RESOURCES=$( ${DOAS} ps -J ${JID} -o 'rss,%cpu' 2> /dev/null | sed 1d ) if [ "${RESOURCES}" != "" ] then while read RSS PER do RAM=$( echo "${RAM} + ${RSS}" | bc -l ) CPU=$( echo "${CPU} + ${PER}" | bc -l ) done << CPU_RAM $( echo "${RESOURCES}" ) CPU_RAM fi # CONVERT KILOBYTES TO MEGABYTES RAM=$( echo "scale=1; ${RAM} / 1024" | bc -l ) RAM=$( printf "%3.1f" "${RAM}" ) CPU=$( printf "%3.1f" "${CPU}" ) # LAST CHECKS BEFORE DISPLAY [ "${JID}" = "" ] && JID='-' [ "${TYPE}" = "" ] && TYPE='-' [ "${DIR}" = "" ] && DIR='-' [ "${IFACE}" = "" ] && IFACE='-' [ "${IPS}" = "" ] && IPS='-' [ "${VER}" = "" ] && VER='-' [ "${CPU}" = "" ] && CPU='-' [ "${RAM}" = "" ] && RAM='-' # PRING GATHERED JAIL INFORMATION echo ${JAIL} ${JID} ${CPU} ${RAM} ${TYPE} ${VER} ${DIR} ${IFACE} ${IPS} # UNSET VARIABLES FOR NEXT LOOP unset JAIL JID TYPE VER DIR IFACE IPS CPU RAM done << JAILS ${JAILS} JAILS # OUTPUT END # OUTPUT FORMATTING BEGIN ) | awk \ '{ # FIND LENGTH FOR EACH COLUMN for(L = 1; L <= NF; L++) { if (length($L) > MAX[L]) { MAX[L] = length($L) } } # STORE EACH COLUMN LENGTH for(R = 1; R <= NF; R++) { ROW[NR, R] = $R } COLS = NF ROWS = NR } END { # BUILD FORMAT STRING FORMAT = "" for(C = 1; C <= COLS; C++) { if (C <= 4) { FORMAT = FORMAT "%" MAX[C] + 1 "s" } else { # ADD SPACE BETWEEN 4TH AND 5TH COLUMNS if (C == 5) { FORMAT = FORMAT " " } FORMAT = FORMAT "%-" MAX[C] + 1 "s" } } FORMAT = FORMAT "\n" # PRINT OUTPUT for(R = 1; R <= ROWS; R++) { ARGS = "" for(C = 1; C <= COLS; C++) { ARGS = ARGS sprintf("%s ", ROW[R, C]) } N = split(ARGS, O) printf FORMAT, O[1], O[2], O[3], O[4], O[5], O[6], O[7], O[8], O[9] } }' # OUTPUT FORMATTING END