├── install.sh ├── LICENSE ├── README.md └── shadow.sh /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Cloning repo..." 4 | git clone --depth 1 https://github.com/ClassicOldSong/shadow.git /tmp/shadow 5 | echo "Installing..." 6 | sudo cp /tmp/shadow/shadow.sh /usr/bin/shadow 7 | sudo chmod 755 /usr/bin/shadow 8 | echo "Removing tmp files..." 9 | rm -rf /tmp/shadow 10 | echo "`/usr/bin/shadow -v` installed!" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yukino Song 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadow 2 | Run shadow clones of your system parallely with Docker 3 | 4 | **!! USE AT YOUR OWN RISK !!** 5 | 6 | ## Requirements 7 | - Linux kernel with OverlayFS support 8 | - Docker 9 | - Bash 10 | - Git (Only for installation) 11 | - Vim (Or other editor $EDITOR sets to) 12 | - Tar (For saving and loading shadow env) 13 | 14 | ## Installation/Upgrade 15 | ``` 16 | curl -L https://git.io/fAnmd | sh 17 | ``` 18 | 19 | ## Usage 20 | ``` 21 | sudo shadow [ARGS...] [CMD...] 22 | ``` 23 | 24 | ## Params 25 | | Arguments | Description | Default | 26 | | ---------------------------- | ----------------------------------------- | ------------------ | 27 | | -h, --help | Show help message | N/A | 28 | | -v, --version | Show version of Shadow | N/A | 29 | | -C, --clean | Clear shadow env in current directory | N/A | 30 | | -s, --start | Start shadow env from Shadowfile | N/A | 31 | | -g, --generate | Generate a Shadowfile | N/A | 32 | | -S, --save | Save current shadow env to a tarball | N/A | 33 | | -L, --load | Load shadow env from a tarball | N/A | 34 | | -U, --upgrade | Upgrade shadow to it's latest version | N/A | 35 | | -q, --quiet, QUIET | Set to disable all shadow logs | (not set) | 36 | | -k, --keep, KEEP_SHADOW_ENV | Set to keep the shadow environment | (not set) | 37 | | -u, --user, START_USER | Start as given username or uid | 0 (root) | 38 | | -w, --work-dir, WORK_DIR | Working directory | (pwd) | 39 | | -i, --ignore, IGNORE_LIST | Paths not to be mounted into a container | dev proc sys | 40 | | -c, --clear, CLEAR_LIST | Paths to clear before container starts | /mnt /run /var/run | 41 | | -f, --file, SHADOW_FILE | Filename of the shadowfile | Shadowfile | 42 | | -I, --img, SHADOW_IMG | Name of the image to be used as base | shadow | 43 | | -p, --perfix, SHADOW_PERFIX | Perfix of the shadow container | SHADOW- | 44 | | -d, --shadow-dir, SHADOW_DIR | Directory where all shadow env file saves | .shadow | 45 | 46 | ## Example 47 | This enters a shadow shell 48 | ``` 49 | sudo shadow 50 | ``` 51 | 52 | This enters a shadow bash shell 53 | ``` 54 | sudo shadow bash 55 | ``` 56 | 57 | This starts python in a shadow environment 58 | ``` 59 | sudo shadow python 60 | ``` 61 | 62 | This starts the shadow system from beginning (may cause tty conflict) 63 | ``` 64 | sudo shadow -w / /sbin/init 65 | ``` 66 | 67 | Run some dangerous commands without actually hurting your system 68 | ``` 69 | sudo shadow rm -rf / --no-preserve-root 70 | ``` 71 | 72 | Keep environment after container detached 73 | ``` 74 | sudo shadow --keep [CMD...] 75 | ``` 76 | 77 | Generate a `Shadowfile` 78 | ``` 79 | shadow [ARGS...] -g 80 | ``` 81 | 82 | Start shadow from `Shadowfile` 83 | ``` 84 | sudo shadow [ARGS...] -s 85 | ``` 86 | 87 | Start shadow from `myShadowfile` 88 | ``` 89 | sudo shadow [ARGS...] -f myShadowfile -s 90 | ``` 91 | 92 | Save a shadow env 93 | ``` 94 | shadow -S shadowenv.tar 95 | ``` 96 | 97 | Save a shadow env with gzip 98 | ``` 99 | shadow -S | gzip -9 > shadowenv.tar.gz 100 | ``` 101 | 102 | Load a shadow env from a tarball to the current directory 103 | ``` 104 | shadow -L shadowenv.tar.gz 105 | ``` 106 | 107 | Load a shadow env from a tarball to another directory 108 | ``` 109 | shadow -L shadowenv.tar.gz /another/directory 110 | ``` 111 | 112 | ## License 113 | MIT 114 | -------------------------------------------------------------------------------- /shadow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Define colors 3 | YELLOW="\033[0;93m" 4 | GREEN="\033[0;32m" 5 | CYAN="\033[0;36m" 6 | RED="\033[0;31m" 7 | NC="\033[0m" 8 | 9 | CMD_NAME=$0 10 | ROOT_LIST=`ls /` 11 | SHADOW_VERSION="v0.3.0" 12 | 13 | QUIET=${QUIET:=""} 14 | KEEP_SHADOW_ENV=${KEEP_SHADOW_ENV:=""} 15 | START_USER=${START_USER:="0"} 16 | WORK_DIR=${WORK_DIR:=$PWD} 17 | IGNORE_LIST=${IGNORE_LIST:="dev proc sys"} 18 | CLEAR_LIST=${CLEAR_LIST:="/mnt /run /var/run"} 19 | SHADOW_FILE=${SHADOW_FILE:="Shadowfile"} 20 | SHADOW_IMG=${SHADOW_IMG:="shadow"} 21 | SHADOW_PERFIX=${SHADOW_PERFIX:="SHADOW-"} 22 | SHADOW_DIR=${SHADOW_DIR:=".shadow"} 23 | SHADOW_ROOT="" 24 | SHADOW_MERGED="" 25 | SHADOW_UPPER="" 26 | SHADOW_WORK="" 27 | SHADOW_LOCK="" 28 | SHADOW_TMP="" 29 | SHADOW_EXISTS="" 30 | 31 | eEcho () { 32 | if [ "$QUIET" == "" ]; then 33 | echo -e "$@" 34 | fi 35 | } 36 | 37 | cEcho () { 38 | eEcho "${CYAN}[SHADOW]${NC} $*" 39 | } 40 | 41 | refreshDirs () { 42 | SHADOW_ROOT="$PWD/$SHADOW_DIR" 43 | SHADOW_MERGED="$SHADOW_ROOT/merged" 44 | SHADOW_UPPER="$SHADOW_ROOT/upper" 45 | SHADOW_WORK="$SHADOW_ROOT/workdir" 46 | SHADOW_LOCK="$SHADOW_ROOT/.shadowlock" 47 | SHADOW_TMP="$SHADOW_MERGED/tmp" 48 | 49 | ls $SHADOW_ROOT > /dev/null 2> /dev/null 50 | SHADOW_EXISTS=$? 51 | } 52 | 53 | # contains method from: 54 | # https://stackoverflow.com/questions/8063228/how-do-i-check-if-a-variable-exists-in-a-list-in-bash 55 | extractVolume () { 56 | for DIR in "$@"; do 57 | # Ignore dirs in ignore list 58 | [[ $IGNORE_LIST =~ (^|[[:space:]])$DIR($|[[:space:]]) ]] \ 59 | && continue \ 60 | || echo -n "-v $SHADOW_MERGED/$DIR:/$DIR " 61 | done 62 | } 63 | 64 | extractGroups () { 65 | for GROUP in `id $START_USER -G`; do 66 | echo -n "--group-add $GROUP " 67 | done 68 | } 69 | 70 | clearDirs () { 71 | for DIR in "$@"; do 72 | SHADOWDIR=$SHADOW_MERGED$DIR 73 | rm -rf $SHADOWDIR 74 | mkdir -p $SHADOWDIR 75 | cEcho "Cleared shadow dir $DIR" 76 | done 77 | } 78 | 79 | prepareEnv () { 80 | if [ "$SHADOW_EXISTS" != "0" ]; then 81 | cEcho "Preparing temp directories at $SHADOW_ROOT ..." 82 | mkdir -p $SHADOW_MERGED $SHADOW_UPPER $SHADOW_WORK 83 | cEcho "Done" 84 | fi 85 | 86 | cEcho "Mounting shadow directories" 87 | mount -t overlay overlay -olowerdir=/,upperdir=$SHADOW_UPPER,workdir=$SHADOW_WORK $SHADOW_MERGED 88 | cEcho "Overlay mounted at $SHADOW_ROOT" 89 | mount -t tmpfs tmpfs -orw,nosuid,nodev $SHADOW_TMP 90 | cEcho "Tmp mounted" 91 | 92 | if [ "SHADOW_EXISTS" != "0" ]; then 93 | clearDirs $CLEAR_LIST 94 | fi 95 | } 96 | 97 | detached () { 98 | if [ -f "$SHADOW_LOCK" ]; then 99 | docker top `cat $SHADOW_LOCK` > /dev/null 2> /dev/null 100 | if [ "$?" == "0" ]; then 101 | cEcho "Container detached, re-enter with \"sudo shadow\" in $PWD" 102 | cEcho "If you would like to keep the shadow env after another attach, set KEEP_SHADOW_ENV to \"YES\" to the environment virables when attaching." 103 | exit 104 | fi 105 | 106 | rm -f $SHADOW_LOCK 107 | cEcho "Container stoped" 108 | umount -l $SHADOW_TMP 109 | cEcho "Tmp unmounted" 110 | umount -l $SHADOW_MERGED 111 | cEcho "Overlay unmounted" 112 | fi 113 | 114 | if [ "$KEEP_SHADOW_ENV" == "" ]; then 115 | rm -rf $SHADOW_ROOT 116 | cEcho "Temp directory cleared, shadow exited" 117 | else 118 | cEcho "Shadow exited, shadow env saved" 119 | cEcho "If you would like to remove the shadow env after another run, unset \"KEEP_SHADOW_ENV\" when starting." 120 | fi 121 | } 122 | 123 | attachContainer () { 124 | eEcho "${GREEN}>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>${NC}" 125 | docker attach `cat $SHADOW_LOCK` 126 | eEcho "\n${GREEN}<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<${NC}" 127 | } 128 | 129 | startContainer () { 130 | cEcho "Starting shadow environment..." 131 | cEcho "${YELLOW}Detach with sequence Ctrl+P, Ctrl+Q${NC}" 132 | 133 | SHADOW_NAME=$SHADOW_PERFIX$RANDOM-$HOSTNAME 134 | echo $SHADOW_NAME > $SHADOW_LOCK 135 | 136 | eEcho "${GREEN}>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>${NC}" 137 | docker run -it --rm --privileged \ 138 | -w $WORK_DIR \ 139 | -u `id $START_USER -u`:`id $START_USER -g` \ 140 | `extractGroups` \ 141 | -e IS_SHADOW=$SHADOW_NAME \ 142 | --name $SHADOW_NAME \ 143 | --hostname $SHADOW_NAME \ 144 | --tmpfs /var/lib/docker:size=2g \ 145 | `extractVolume $ROOT_LIST` \ 146 | $SHADOW_IMG "$@" 147 | eEcho "\n${GREEN}<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<${NC}" 148 | } 149 | 150 | runContainer () { 151 | # Ask whether to attach if shadow is running 152 | if [ -f "$SHADOW_LOCK" ]; then 153 | # Prompt script from: 154 | # https://stackoverflow.com/questions/226703/how-do-i-prompt-for-yes-no-cancel-input-in-a-linux-shell-script 155 | echo -e "${CYAN}[SHADOW]${NC} Shadow in this dir is already running, attatch?" 156 | select yn in "Yes" "No"; do 157 | case $yn in 158 | Yes ) attachContainer; break;; 159 | No ) exit;; 160 | esac 161 | done 162 | else 163 | prepareEnv 164 | startContainer "$@" 165 | fi 166 | 167 | # Run detached after all kinds of attachment 168 | detached 169 | } 170 | 171 | # Args 172 | # Show version 173 | showVersion () { 174 | echo "Shadow $SHADOW_VERSION" 175 | exit 176 | } 177 | 178 | # Show help 179 | showHelp () { 180 | echo "Usage: shadow [ARGS...] [CMD...] 181 | 182 | | Arguments | Description | Default | 183 | | ---------------------------- | ----------------------------------------- | ------------------ | 184 | | -h, --help | Show help message | N/A | 185 | | -v, --version | Show version of Shadow | N/A | 186 | | -C, --clean | Clear shadow env in current directory | N/A | 187 | | -s, --start | Start shadow env from Shadowfile | N/A | 188 | | -g, --generate | Generate a Shadowfile | N/A | 189 | | -S, --save | Save current shadow env to a tarball | N/A | 190 | | -L, --load | Load shadow env from a tarball | N/A | 191 | | -U, --upgrade | Upgrade shadow to it's latest version | N/A | 192 | | -q, --quiet, QUIET | Set to disable all shadow logs | (not set) | 193 | | -k, --keep, KEEP_SHADOW_ENV | Set to keep the shadow environment | (not set) | 194 | | -u, --user, START_USER | Start as given username or uid | 0 (root) | 195 | | -w, --work-dir, WORK_DIR | Working directory | (pwd) | 196 | | -i, --ignore, IGNORE_LIST | Paths not to be mounted into a container | dev proc sys | 197 | | -c, --clear, CLEAR_LIST | Paths to clear before container starts | /mnt /run /var/run | 198 | | -f, --file, SHADOW_FILE | Filename of the shadowfile | Shadowfile | 199 | | -I, --img, SHADOW_IMG | Name of the image to be used as base | shadow | 200 | | -p, --perfix, SHADOW_PERFIX | Perfix of the shadow container | SHADOW- | 201 | | -d, --shadow-dir, SHADOW_DIR | Directory where all shadow env file saves | .shadow | 202 | 203 | Read more at https://github.com/ClassicOldSong/shadow/blob/master/README.md 204 | 205 | Report bugs at https://github.com/ClassicOldSong/shadow/issues/new" 206 | 207 | exit 208 | } 209 | 210 | # Clean current shadow env 211 | cleanShadow () { 212 | # Set KEEP_SHADOW_ENV empty 213 | KEEP_SHADOW_ENV="" 214 | 215 | if [ "$SHADOW_EXISTS" != "0" ]; then 216 | cEcho "Shadow not exists, exit" 217 | exit 218 | fi 219 | 220 | if [ -f "$SHADOW_LOCK" ]; then 221 | cEcho "Stopping container..." 222 | docker kill `cat $SHADOW_LOCK` > /dev/null 2> /dev/null 223 | fi 224 | 225 | detached 226 | exit 227 | } 228 | 229 | # Start with Shadowfile 230 | startShadow () { 231 | if [ ! -f "$SHADOW_FILE" ]; then 232 | cEcho "$SHADOW_FILE not found, exit" 233 | exit 1 234 | fi 235 | 236 | cEcho "Starting shadow with $SHADOW_FILE..." 237 | . $SHADOW_FILE 238 | # Refresh shadow dirs after shadowfile loaded 239 | refreshDirs 240 | runContainer "${CMD[@]}" 241 | exit 242 | } 243 | 244 | generateShadowfile () { 245 | if [ -f "$SHADOW_FILE" ]; then 246 | echo "$SHADOW_FILE exists, remove it before generating a new one." 247 | exit 1 248 | fi 249 | 250 | echo "#!/bin/bash 251 | 252 | QUIET=\"$QUIET\" 253 | KEEP_SHADOW_ENV=\"$KEEP_SHADOW_ENV\" 254 | START_USER=\"$START_USER\" 255 | WORK_DIR=\"$WORK_DIR\" 256 | IGNORE_LIST=\"$IGNORE_LIST\" 257 | CLEAR_LIST=\"$CLEAR_LIST\" 258 | SHADOW_IMG=\"$SHADOW_IMG\" 259 | SHADOW_PERFIX=\"$SHADOW_PERFIX\" 260 | SHADOW_DIR=\"$SHADOW_DIR\" 261 | CMD=(\"bash\" \"-c\" \"echo \\\"Change the CMD section of the $SHADOW_FILE to your custom command\\\"\") 262 | " > $SHADOW_FILE 263 | 264 | # Start the editor which user specified 265 | ${EDITOR:-vim} $SHADOW_FILE 266 | exit 267 | } 268 | 269 | upgradeShadow () { 270 | echo "Upgrading Shadow..." 271 | curl -L https://git.io/fAnmd | sh 272 | exit 273 | } 274 | 275 | saveShadowEnv () { 276 | if [ "$SHADOW_EXISTS" != "0" ]; then 277 | cEcho "Shadow not exists, exit" 278 | exit 279 | fi 280 | 281 | if [ "$1" ]; then 282 | cEcho "Saving shadow env..." 283 | tar cf "$1" --exclude="$SHADOW_DIR/.shadowlock" --one-file-system $SHADOW_DIR $SHADOW_FILE 2> /dev/null 284 | cEcho "Shadow env saved as $1" 285 | else 286 | tar cf - --exclude="$SHADOW_DIR/.shadowlock" --one-file-system $SHADOW_DIR $SHADOW_FILE 2> /dev/null | cat 287 | fi 288 | exit 289 | } 290 | 291 | loadShadowEnv () { 292 | LOAD_DIR=${2:-$PWD} 293 | 294 | if [ -d "$LOAD_DIR/$SHADOW_DIR" ]; then 295 | cEcho "Shadow already exists in $LOAD_DIR, clean with \"$CMD_NAME --clean\" in $LOAD_DIR and then try to load another" 296 | exit 297 | fi 298 | 299 | mkdir -p $LOAD_DIR 300 | cEcho "Loading shadow env..." 301 | tar xf $1 -C $LOAD_DIR 302 | cEcho "Shadow env loaded to $LOAD_DIR" 303 | exit 304 | } 305 | 306 | # Arg flags 307 | FLAG_START="" 308 | FLAG_CLEAN="" 309 | FLAG_GENERATE="" 310 | FLAG_SAVEENV="" 311 | FLAG_LOADENV="" 312 | 313 | # Parse arguments: 314 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 315 | while [[ $# -gt 0 ]] 316 | do 317 | key="$1" 318 | 319 | case $key in 320 | -h|--help) 321 | showHelp 322 | ;; 323 | -v|--version) 324 | showVersion 325 | ;; 326 | -C|--clean) 327 | FLAG_CLEAN="YES" 328 | shift 329 | ;; 330 | -s|--start) 331 | FLAG_START="YES" 332 | shift 333 | ;; 334 | -g|--generate) 335 | FLAG_GENERATE="YES" 336 | shift 337 | ;; 338 | -S|--save) 339 | FLAG_SAVEENV="YES" 340 | shift 341 | ;; 342 | -L|--load) 343 | FLAG_LOADENV="YES" 344 | shift 345 | ;; 346 | -U|--upgrade) 347 | upgradeShadow 348 | ;; 349 | -q|--quiet) 350 | QUIET="YES" 351 | shift 352 | ;; 353 | -k|--keep) 354 | KEEP_SHADOW_ENV="YES" 355 | shift 356 | ;; 357 | -u|--user) 358 | START_USER=$2 359 | shift 360 | shift 361 | ;; 362 | -w|--work-dir) 363 | WORK_DIR=$2 364 | shift 365 | shift 366 | ;; 367 | -i|--ignore) 368 | IGNORE_LIST=$2 369 | shift 370 | shift 371 | ;; 372 | -c|--clear) 373 | CLEAR_LIST=$2 374 | shift 375 | shift 376 | ;; 377 | -f|--file) 378 | SHADOW_FILE=$2 379 | shift 380 | shift 381 | ;; 382 | -I|--img) 383 | SHADOW_IMG=$2 384 | shift 385 | shift 386 | ;; 387 | -p|--perfix) 388 | SHADOW_PERFIX=$2 389 | shift 390 | shift 391 | ;; 392 | -d|--shadow-dir) 393 | SHADOW_DIR=$2 394 | shift 395 | shift 396 | ;; 397 | *) 398 | if [[ $key == -* ]]; then 399 | echo "Unknown parameter \"$key\". Show help with \"$CMD_NAME --help\"" 400 | exit 1 401 | else 402 | break 403 | fi 404 | ;; 405 | esac 406 | done 407 | 408 | # Refresh shadow dirs 409 | refreshDirs 410 | 411 | if [ "$FLAG_CLEAN" ]; then cleanShadow; fi 412 | if [ "$FLAG_GENERATE" ]; then generateShadowfile; fi 413 | if [ "$FLAG_SAVEENV" ]; then saveShadowEnv "$@"; fi 414 | if [ "$FLAG_LOADENV" ]; then loadShadowEnv "$@"; fi 415 | 416 | # Check if this is a shadow already 417 | if [ "$IS_SHADOW" ]; then 418 | cEcho "$IS_SHADOW is already inside a shadow, exit" 419 | exit 1 420 | fi 421 | 422 | if [ "$FLAG_START" ]; then startShadow; fi 423 | 424 | # Build shadow image if not exists 425 | docker images | grep $SHADOW_IMG > /dev/null 426 | if [ "$?" != "0" ]; then 427 | BUILD_DIR="/tmp/shadow_build" 428 | 429 | cEcho "Shadow image \"$SHADOW_IMG\" not found, trying to build..." 430 | 431 | mkdir -p $BUILD_DIR 432 | echo -e "FROM scratch\nCMD /bin/sh\n" > $BUILD_DIR/Dockerfile 433 | 434 | pushd $BUILD_DIR > /dev/null 435 | docker build -t $SHADOW_IMG . > /dev/null 436 | popd > /dev/null 437 | 438 | rm -rf $BUILD_DIR 439 | cEcho "Build complete" 440 | fi 441 | 442 | # Start container 443 | runContainer "$@" 444 | --------------------------------------------------------------------------------