diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6c7b69a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 152185a..983cb6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends binfmt-support qemu-user-static + - run: ./build.sh - - run: ./test.sh shellsu-amd64 - - run: ./test.sh shellsu-i386 - - run: ./test.sh --debian shellsu-amd64 - - run: ./test.sh --debian shellsu-i386 + - run: ./test.sh --alpine shellsu + - run: ./test.sh --debian shellsu - run: docker build --pull --file hub/Dockerfile.alpine hub - run: docker build --pull --file hub/Dockerfile.debian hub diff --git a/Dockerfile b/Dockerfile index a2c9d27..e8ce417 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,50 +5,25 @@ FROM debian:bookworm-slim RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ - arch-test \ file \ bash \ + login \ ; \ rm -rf /var/lib/apt/lists/* # Set environment variable for build flags ENV BUILD_FLAGS="-v" -# Prepare the `shellsu` build and test script -RUN set -eux; \ - { \ - echo '#!/usr/bin/env bash'; \ - echo 'set -Eeuo pipefail -x'; \ - echo 'ARCH="${ARCH:-$(uname -m)}"'; \ - echo 'cp /usr/local/bin/shellsu /usr/local/bin/shellsu-$ARCH'; \ - echo 'file "/usr/local/bin/shellsu-$ARCH"'; \ - echo 'if arch-test "$ARCH"; then'; \ - echo ' try() { for (( i = 0; i < 30; i++ )); do if timeout 1s "$@"; then return 0; fi; done; return 1; }'; \ - echo ' try "/usr/local/bin/shellsu-$ARCH" --version'; \ - echo ' try "/usr/local/bin/shellsu-$ARCH" nobody id'; \ - echo ' try "/usr/local/bin/shellsu-$ARCH" nobody ls -l /proc/self/fd'; \ - echo 'fi'; \ - } > /usr/local/bin/shellsu-build-and-test.sh; \ - chmod +x /usr/local/bin/shellsu-build-and-test.sh - -# Disable CGO (not relevant for Bash, but keeping for alignment with original pattern) -ENV CGO_ENABLED 0 - # Copy `shellsu` script into the container WORKDIR /usr/local/bin -COPY shellsu /usr/local/bin/shellsu +COPY shellsu.sh /usr/local/bin/shellsu RUN chmod +x /usr/local/bin/shellsu -# Test `shellsu` for various architectures -RUN ARCH=amd64 shellsu-build-and-test.sh -RUN ARCH=i386 shellsu-build-and-test.sh -RUN ARCH=armel shellsu-build-and-test.sh -RUN ARCH=armhf shellsu-build-and-test.sh -RUN ARCH=arm64 shellsu-build-and-test.sh -RUN ARCH=mips64el shellsu-build-and-test.sh -RUN ARCH=ppc64el shellsu-build-and-test.sh -RUN ARCH=riscv64 shellsu-build-and-test.sh -RUN ARCH=s390x shellsu-build-and-test.sh +# Test `shellsu` to verify functionality +RUN set -eux; \ + # Run the script with basic tests + ./shellsu --help; \ + ./shellsu --version -# Final verification step -RUN set -eux; ls -lAFh /usr/local/bin/shellsu-*; file /usr/local/bin/shellsu-* +# Final verification step (list files and check permissions) +RUN set -eux; ls -lAFh /usr/local/bin/shellsu; file /usr/local/bin/shellsu diff --git a/Dockerfile.test-alpine b/Dockerfile.test-alpine index e16c359..3563bf6 100644 --- a/Dockerfile.test-alpine +++ b/Dockerfile.test-alpine @@ -1,40 +1,49 @@ FROM alpine:3.20 +RUN apk add --no-cache bash coreutils shadow + # Add "nobody" to ALL groups to create edge cases for testing RUN cut -d: -f1 /etc/group | xargs -rtn1 addgroup nobody # Create the `shellsu-t` testing script RUN { \ - echo '#!/bin/sh'; \ - echo 'set -ex'; \ - echo; \ - echo 'spec="$1"; shift'; \ - echo; \ - echo 'expec="$1"; shift'; \ - echo 'real="$(shellsu "$spec" id -u):$(shellsu "$spec" id -g):$(shellsu "$spec" id -G)"'; \ - echo '[ "$expec" = "$real" ]'; \ - echo; \ - echo 'expec="$1"; shift'; \ - echo 'real="$(shellsu "$spec" id -un):$(shellsu "$spec" id -gn):$(shellsu "$spec" id -Gn)" || true'; \ - echo '[ "$expec" = "$real" ]'; \ - } > /usr/local/bin/shellsu-t \ - && chmod +x /usr/local/bin/shellsu-t + echo '#!/bin/sh'; \ + echo 'set -ex'; \ + echo; \ + echo 'spec="$1"; shift'; \ + echo; \ + echo 'expec="$1"; shift'; \ + echo 'real="$(/usr/local/bin/shellsu "$spec" id -u):$(/usr/local/bin/shellsu "$spec" id -g):$(/usr/local/bin/shellsu "$spec" id -G)"'; \ + echo '[ "$expec" = "$real" ] || { echo "Test failed"; exit 1; }'; \ + echo; \ + echo 'expec="$1"; shift'; \ + echo 'real="$(/usr/local/bin/shellsu "$spec" id -un):$(/usr/local/bin/shellsu "$spec" id -gn):$(/usr/local/bin/shellsu "$spec" id -Gn)" || true'; \ + echo '[ "$expec" = "$real" ] || { echo "Test failed"; exit 1; }'; \ +} > /usr/local/bin/shellsu-t \ +&& chmod +x /usr/local/bin/shellsu-t # Copy the `shellsu` binary/script into the image -COPY shellsu /usr/local/bin/ +RUN ls -la +RUN pwd +COPY shellsu /usr/local/bin/shellsu RUN chmod +x /usr/local/bin/shellsu +RUN ls -la /usr/local/bin/ +RUN echo $PATH +RUN ls -l $(which su) # Adjust permissions for testing unusual cases RUN chgrp nobody /usr/local/bin/shellsu \ && chmod +s /usr/local/bin/shellsu # Configure an environment for testing -ENV SHELLSU_INSECURE_MODE="I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." +ENV SHELLSU_INSECURE_FLAG="I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." USER nobody ENV HOME /omg/really/shellsu/nowhere # Validate initial state RUN id +RUN cat /etc/passwd +RUN cat /etc/group # Test various user/group configurations RUN shellsu-t 0 "0:0:$(id -G root)" "root:root:$(id -Gn root)" diff --git a/Dockerfile.test-debian b/Dockerfile.test-debian index bd82225..304055c 100644 --- a/Dockerfile.test-debian +++ b/Dockerfile.test-debian @@ -1,5 +1,16 @@ FROM debian:bookworm-slim +# Install necessary dependencies +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + file \ + bash \ + login \ + ; \ + rm -rf /var/lib/apt/lists/* + + # Add "nobody" to ALL groups to create edge cases for testing RUN cut -d: -f1 /etc/group | xargs -rtI'{}' usermod -aG '{}' nobody # Emulate Alpine's "games" user, which is part of the "users" group @@ -13,30 +24,38 @@ RUN { \ echo 'spec="$1"; shift'; \ echo; \ echo 'expec="$1"; shift'; \ - echo 'real="$(shellsu "$spec" id -u):$(shellsu "$spec" id -g):$(shellsu "$spec" id -G)"'; \ + echo 'real="$(/usr/local/bin/shellsu "$spec" id -u):$(/usr/local/bin/shellsu "$spec" id -g):$(/usr/local/bin/shellsu "$spec" id -G)"'; \ echo '[ "$expec" = "$real" ]'; \ echo; \ echo 'expec="$1"; shift'; \ - echo 'real="$(shellsu "$spec" id -un):$(shellsu "$spec" id -gn):$(shellsu "$spec" id -Gn)" || true'; \ + echo 'real="$(/usr/local/bin/shellsu "$spec" id -un):$(/usr/local/bin/shellsu "$spec" id -gn):$(/usr/local/bin/shellsu "$spec" id -Gn)" || true'; \ echo '[ "$expec" = "$real" ]'; \ } > /usr/local/bin/shellsu-t \ && chmod +x /usr/local/bin/shellsu-t # Copy the `shellsu` binary/script into the image -COPY shellsu /usr/local/bin/ +RUN ls -la +RUN pwd +COPY shellsu /usr/local/bin/shellsu +COPY shellsu /usr/local/bin/shellsu RUN chmod +x /usr/local/bin/shellsu +RUN ls -la /usr/local/bin/ +RUN echo $PATH +RUN ls -l $(which su) # Adjust permissions for testing unusual cases RUN chgrp nogroup /usr/local/bin/shellsu \ && chmod +s /usr/local/bin/shellsu # Configure an environment for testing -ENV SHELLSU_INSECURE_MODE="I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." +ENV SHELLSU_INSECURE_FLAG="I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." USER nobody ENV HOME /omg/really/shellsu/nowhere # Validate initial state RUN id +RUN cat /etc/passwd +RUN cat /etc/group # Test various user/group configurations RUN shellsu-t 0 "0:0:$(id -G root)" "root:root:$(id -Gn root)" diff --git a/build.sh b/build.sh index 00b169c..519f3c9 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # Navigate to the directory containing this script @@ -7,20 +7,17 @@ cd "$(dirname "$(readlink -f "$BASH_SOURCE")")" set -x # Build the Docker image -docker build --pull -t shellsu . +docker build --no-cache --pull -t shellsu . -# Clean up any pre-existing artifacts rm -f shellsu* SHA256SUMS* -# Extract the `shellsu` binaries from the built image -docker run --rm shellsu sh -c 'cd /go/bin && tar -c shellsu*' | tar -xv +docker run --rm --entrypoint cat shellsu /usr/local/bin/shellsu > shellsu -# Generate SHA256 checksums for the extracted files -sha256sum shellsu* | tee SHA256SUMS +chmod +x shellsu -# Inspect the extracted files -file shellsu* -ls -lFh shellsu* SHA256SUMS* +sha256sum shellsu | tee SHA256SUMS -# Run the built binary to verify functionality -"./shellsu-$(dpkg --print-architecture)" --help +file shellsu +ls -lFh shellsu SHA256SUMS + +"./shellsu" --help diff --git a/hub/Dockerfile.alpine b/hub/Dockerfile.alpine index 652bad6..f279608 100644 --- a/hub/Dockerfile.alpine +++ b/hub/Dockerfile.alpine @@ -1,6 +1,6 @@ FROM alpine:3.20 -# https://github.com/tianon/shellsu/releases +# https://github.com/dogruis/shellsu/releases ENV SHELLSU_VERSION 1.17 RUN set -eux; \ diff --git a/hub/Dockerfile.debian b/hub/Dockerfile.debian index 891ed8f..fec06aa 100644 --- a/hub/Dockerfile.debian +++ b/hub/Dockerfile.debian @@ -1,6 +1,6 @@ FROM debian:bookworm-slim -# https://github.com/tianon/shellsu/releases +# https://github.com/dogruis/shellsu/releases ENV SHELLSU_VERSION 1.17 RUN set -eux; \ diff --git a/shellsu.sh b/shellsu.sh index f28c60f..f1dccfd 100755 --- a/shellsu.sh +++ b/shellsu.sh @@ -4,16 +4,20 @@ set -Eeuo pipefail # Script Metadata VERSION="1.0" SCRIPT_NAME="$(basename "$0")" -LICENSE_TEXT="Apache-2.0 (see https://github.com/tianon/gosu)" +LICENSE_TEXT="MIT (see https://github.com/dogruis/shellsu)" # Helper Functions usage() { cat </dev/null || error_exit "User '$USER' does not exist." - if [[ -n "$GROUP" ]]; then - getent group "$GROUP" &>/dev/null || error_exit "Group '$GROUP' does not exist." +# Check if the script is insecure (no setuid/setgid bit) +check_insecure() { + if [[ "${SHELLSU_INSECURE_FLAG:-}" != "I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain. Time to die." ]]; then + # Check if the script has setuid or setgid permissions + if [[ -u "$0" ]]; then + error_exit "$0 appears to be installed with the 'setuid' bit set, which is extremely insecure and unsupported! Use 'sudo' or 'su' instead." + elif [[ -g "$0" ]]; then + error_exit "$0 appears to be installed with the 'setgid' bit set, which is not great and unsupported! Use 'sudo' or 'su' instead." + fi fi } -# Switch user and group, then execute the command -execute_as_user() { - local user="$1" - local group="$2" - shift 2 - local cmd=("$@") +# Function to set up the user and group context +setup_user() { + local user_spec="$1" + local user_name + local user_uid + local user_gid + local user_home + local group_ids=() - # Resolve group ID if specified - if [[ -n "$group" ]]; then - GROUP_ID=$(getent group "$group" | cut -d: -f3) - sg "$group" <&2; exit 1; } -[ -f "$shellsu" ] || { usage >&2; exit 1; } +shift || { echo "Error: Missing shellsu script argument." >&2; usage >&2; exit 1; } +[ -f "$shellsu" ] || { echo "Error: shellsu script '$shellsu' does not exist." >&2; usage >&2; exit 1; } -trap '{ set +x; echo; echo FAILED; echo; } >&2' ERR - -set -x +echo "Verifying Dockerfile: $df" +[ -f "$df" ] || { echo "Error: Dockerfile '$df' does not exist." >&2; exit 1; } dir="$(mktemp -d -t shellsu-test-XXXXXXXXXX)" base="$(basename "$dir")" img="shellsu-test:$base" trap "rm -rf '$dir'" EXIT -cp -T "$df" "$dir/Dockerfile" -cp -T "$shellsu" "$dir/shellsu" -docker build -t "$img" "$dir" -rm -rf "$dir" + +echo "Using temporary directory: $dir" +echo "Docker image will be tagged as: $img" +echo "Copying shellsu script '$shellsu' to $dir/shellsu" +echo "Copying Dockerfile '$df' to $dir/Dockerfile" + +cp -T "$df" "$dir/Dockerfile" || { echo "Error: Failed to copy Dockerfile to '$dir/Dockerfile'." >&2; exit 1; } +cp -T "$shellsu" "$dir/shellsu" || { echo "Error: Failed to copy '$shellsu' to '$dir/shellsu'." >&2; exit 1; } + +echo "Building Docker image '$img' using Dockerfile '$df'..." +docker build -t "$img" "$dir" || { echo "Error: Failed to build Docker image '$img'." >&2; exit 1; } +rm -rf "$dir" # Clean up the temporary directory trap - EXIT trap "docker rm -f '$base' > /dev/null; docker rmi -f '$img' > /dev/null" EXIT -# using explicit "--init=false" in case dockerd is running with "--init" (because that will skew our process numbers) -docker run -d --init=false --name "$base" "$img" shellsu root sleep 1000 -sleep 1 # give it plenty of time to get through "shellsu" and into the "sleep" -[ "$(docker top "$base" | wc -l)" = 2 ] -# "docker top" should have only two lines -# -- ps headers and a single line for the single process running in the container +echo "Testing shellsu script with --help and --version..." +docker run --rm "$img" /usr/local/bin/shellsu --help || { echo "Error: Failed to run '/usr/local/bin/shellsu --help' inside the container." >&2; exit 1; } +docker run --rm "$img" /usr/local/bin/shellsu --version || { echo "Error: Failed to run '/usr/local/bin/shellsu --version' inside the container." >&2; exit 1; } + +echo "Running the shellsu script with sleep for basic functionality check..." +docker run -d --init=false --name "$base" "$img" /usr/local/bin/shellsu root sleep 1000 || { echo "Error: Failed to start container with '/usr/local/bin/shellsu root sleep 1000'." >&2; exit 1; } + +sleep 1 # Allow some time for the script to run +echo "Checking process count..." +process_count=$(docker top "$base" | wc -l) +if [ "$process_count" -ne 2 ]; then + echo "Error: Unexpected number of processes in container (expected 2, found $process_count)." >&2 + exit 1 +fi + +echo "Test passed successfully!"