From 64ad58bd616777ef49067e1315b1cbf6fc1110c2 Mon Sep 17 00:00:00 2001 From: zonyitoo Date: Wed, 29 Jun 2022 00:31:57 +0800 Subject: [PATCH] switch user with -a, docker adds entrypoint script, #876 --- .github/workflows/build-docker-image.yml | 6 +- Dockerfile.v2ray | 15 ----- README.md | 4 +- Dockerfile => docker/Dockerfile | 20 +++--- docker/Dockerfile.v2ray | 16 +++++ docker/docker-entrypoint.sh | 38 ++++++++++++ src/service/local.rs | 20 +++++- src/service/manager.rs | 20 +++++- src/service/server.rs | 20 +++++- src/sys.rs | 78 ++++++++++++++++++++++++ 10 files changed, 206 insertions(+), 31 deletions(-) delete mode 100644 Dockerfile.v2ray rename Dockerfile => docker/Dockerfile (76%) create mode 100644 docker/Dockerfile.v2ray create mode 100755 docker/docker-entrypoint.sh diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index fd274c62..52f9a47e 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - bin: + bin: - ssserver - sslocal steps: @@ -17,7 +17,7 @@ jobs: - name: Setup Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -34,3 +34,5 @@ jobs: target: ${{ matrix.bin }} tags: ${{ steps.metadata.outputs.tags }} push: true + context: . + file: ./docker/Dockerfile diff --git a/Dockerfile.v2ray b/Dockerfile.v2ray deleted file mode 100644 index d2e1e0c0..00000000 --- a/Dockerfile.v2ray +++ /dev/null @@ -1,15 +0,0 @@ -FROM ghcr.io/shadowsocks/ssserver-rust:latest - -USER root - -RUN cd /tmp && \ - TAG=$(wget -qO- https://api.github.com/repos/shadowsocks/v2ray-plugin/releases/latest | grep tag_name | cut -d '"' -f4) && \ - wget https://github.com/shadowsocks/v2ray-plugin/releases/download/$TAG/v2ray-plugin-linux-amd64-$TAG.tar.gz && \ - tar -xf *.gz && \ - rm *.gz && \ - mv v2ray* /usr/bin/v2ray-plugin && \ - chmod +x /usr/bin/v2ray-plugin - -USER nobody - -ENTRYPOINT [ "ssserver", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] diff --git a/README.md b/README.md index 866611e0..66e7bd96 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ docker pull ghcr.io/shadowsocks/ssserver-rust:latest If you want to build the Docker image yourself, you need to use the [BuildX](https://docs.docker.com/buildx/working-with-buildx/). ```bash -docker buildx build -t shadowsocks/ssserver-rust:latest -t shadowsocks/ssserver-rust:v1.11.1 --target ssserver . -docker buildx build -t shadowsocks/sslocal-rust:latest -t shadowsocks/sslocal-rust:v1.11.1 --target sslocal . +docker buildx build -t shadowsocks/ssserver-rust:latest -t shadowsocks/ssserver-rust:v1.11.1 --target ssserver ./docker +docker buildx build -t shadowsocks/sslocal-rust:latest -t shadowsocks/sslocal-rust:v1.11.1 --target sslocal ./docker ``` #### Run the container diff --git a/Dockerfile b/docker/Dockerfile similarity index 76% rename from Dockerfile rename to docker/Dockerfile index a98d58d7..c649628e 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -10,22 +10,22 @@ ADD . . RUN rustup install nightly && rustup default nightly && \ case "$TARGETARCH" in \ - "386") \ + "386") \ RUST_TARGET="i686-unknown-linux-musl" \ MUSL="i686-linux-musl" \ - ;; \ + ;; \ "amd64") \ RUST_TARGET="x86_64-unknown-linux-musl" \ MUSL="x86_64-linux-musl" \ - ;; \ + ;; \ "arm64") \ RUST_TARGET="aarch64-unknown-linux-musl" \ MUSL="aarch64-linux-musl" \ - ;; \ + ;; \ *) \ echo "Doesn't support $TARGETARCH architecture" \ exit 1 \ - ;; \ + ;; \ esac && \ wget -qO- "https://musl.cc/$MUSL-cross.tgz" | tar -xzC /root/ && \ CC=/root/$MUSL-cross/bin/$MUSL-gcc && \ @@ -36,19 +36,21 @@ RUN rustup install nightly && rustup default nightly && \ FROM alpine:3.14 AS sslocal COPY --from=build /root/shadowsocks-rust/target/release/sslocal /usr/bin - COPY --from=build /root/shadowsocks-rust/examples/config.json /etc/shadowsocks-rust/ +COPY --from=build /root/shadowsocks-rust/docker/docker-entrypoint.sh / USER nobody -ENTRYPOINT [ "sslocal", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] +ENTRYPOINT [ "/docker-entrypoint.sh" ] +CMD [ "sslocal", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] FROM alpine:3.14 AS ssserver COPY --from=build /root/shadowsocks-rust/target/release/ssserver /usr/bin - COPY --from=build /root/shadowsocks-rust/examples/config.json /etc/shadowsocks-rust/ +COPY --from=build /root/shadowsocks-rust/docker/docker-entrypoint.sh / USER nobody -ENTRYPOINT [ "ssserver", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] +ENTRYPOINT [ "/docker-entrypoint.sh" ] +CMD [ "ssserver", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] diff --git a/docker/Dockerfile.v2ray b/docker/Dockerfile.v2ray new file mode 100644 index 00000000..19412e55 --- /dev/null +++ b/docker/Dockerfile.v2ray @@ -0,0 +1,16 @@ +FROM ghcr.io/shadowsocks/ssserver-rust:latest + +USER root + +RUN cd /tmp && \ + TAG=$(wget -qO- https://api.github.com/repos/shadowsocks/v2ray-plugin/releases/latest | grep tag_name | cut -d '"' -f4) && \ + wget https://github.com/shadowsocks/v2ray-plugin/releases/download/$TAG/v2ray-plugin-linux-amd64-$TAG.tar.gz && \ + tar -xf *.gz && \ + rm *.gz && \ + mv v2ray* /usr/bin/v2ray-plugin && \ + chmod +x /usr/bin/v2ray-plugin + +USER nobody + +ENTRYPOINT [ "/docker-entrypoint.sh" ] +CMD [ "ssserver", "--log-without-time", "-c", "/etc/shadowsocks-rust/config.json" ] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 00000000..5a6458ee --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +if [ -z "${SS_ENTRYPOINT_QUIET_LOGS:-}" ]; then + exec 3>&1 +else + exec 3>/dev/null +fi + +if [ "$1" = "sslocal" -o "$1" = "ssserver" -o "$1" = "ssmanager" -o "$1" = "ssservice" ]; then + if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + echo >&3 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" + + echo >&3 "$0: Looking for shell scripts in /docker-entrypoint.d/" + find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.sh) + if [ -x "$f" ]; then + echo >&3 "$0: Launching $f"; + "$f" + else + # warn on shell scripts without exec bit + echo >&3 "$0: Ignoring $f, not executable"; + fi + ;; + *) echo >&3 "$0: Ignoring $f";; + esac + done + + echo >&3 "$0: Configuration complete; ready for start up" + else + echo >&3 "$0: No files found in /docker-entrypoint.d/, skipping configuration" + fi +fi + +exec "$@" diff --git a/src/service/local.rs b/src/service/local.rs index 5b648dcc..f977dbe4 100644 --- a/src/service/local.rs +++ b/src/service/local.rs @@ -27,7 +27,8 @@ use shadowsocks_service::{ use crate::logging; use crate::{ config::{Config as ServiceConfig, RuntimeMode}, - monitor, validator, + monitor, + validator, }; /// Defines command line options @@ -371,6 +372,17 @@ pub fn define_command_line_options(mut app: Command<'_>) -> Command<'_> { ); } + #[cfg(unix)] + { + app = app.arg( + Arg::new("USER") + .long("user") + .short('a') + .takes_value(true) + .help("Run as another user"), + ); + } + app } @@ -776,6 +788,12 @@ pub fn main(matches: &ArgMatches) -> ExitCode { daemonize::daemonize(matches.value_of("DAEMONIZE_PID_PATH")); } + #[cfg(unix)] + if let Some(uname) = matches.value_of("USER") { + crate::sys::run_as_user(uname); + } + crate::sys::check_run_from_root(); + info!("shadowsocks local {} build {}", crate::VERSION, crate::BUILD_TIME); let mut builder = match service_config.runtime.mode { diff --git a/src/service/manager.rs b/src/service/manager.rs index 81e3da06..54ab5dac 100644 --- a/src/service/manager.rs +++ b/src/service/manager.rs @@ -24,7 +24,8 @@ use shadowsocks_service::{ use crate::logging; use crate::{ config::{Config as ServiceConfig, RuntimeMode}, - monitor, validator, + monitor, + validator, }; /// Defines command line options @@ -204,6 +205,17 @@ pub fn define_command_line_options(mut app: Command<'_>) -> Command<'_> { ); } + #[cfg(unix)] + { + app = app.arg( + Arg::new("USER") + .long("user") + .short('a') + .takes_value(true) + .help("Run as another user"), + ); + } + app } @@ -448,6 +460,12 @@ pub fn main(matches: &ArgMatches) -> ExitCode { daemonize::daemonize(matches.value_of("DAEMONIZE_PID_PATH")); } + #[cfg(unix)] + if let Some(uname) = matches.value_of("USER") { + crate::sys::run_as_user(uname); + } + crate::sys::check_run_from_root(); + info!("shadowsocks manager {} build {}", crate::VERSION, crate::BUILD_TIME); let mut worker_count = 1; diff --git a/src/service/server.rs b/src/service/server.rs index 72a9f082..2df641d5 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -22,7 +22,8 @@ use shadowsocks_service::{ use crate::logging; use crate::{ config::{Config as ServiceConfig, RuntimeMode}, - monitor, validator, + monitor, + validator, }; /// Defines command line options @@ -216,6 +217,17 @@ pub fn define_command_line_options(mut app: Command<'_>) -> Command<'_> { ); } + #[cfg(unix)] + { + app = app.arg( + Arg::new("USER") + .long("user") + .short('a') + .takes_value(true) + .help("Run as another user"), + ); + } + app } @@ -461,6 +473,12 @@ pub fn main(matches: &ArgMatches) -> ExitCode { daemonize::daemonize(matches.value_of("DAEMONIZE_PID_PATH")); } + #[cfg(unix)] + if let Some(uname) = matches.value_of("USER") { + crate::sys::run_as_user(uname); + } + crate::sys::check_run_from_root(); + info!("shadowsocks server {} build {}", crate::VERSION, crate::BUILD_TIME); let mut worker_count = 1; diff --git a/src/sys.rs b/src/sys.rs index 58501742..01b32d40 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -74,3 +74,81 @@ pub fn adjust_nofile() { } } } + +/// setuid(), setgid() for a specific user or uid +#[cfg(unix)] +pub fn run_as_user(uname: &str) { + use log::warn; + use std::{ + ffi::{CStr, CString}, + io::Error, + }; + + unsafe { + let pwd = match uname.parse::() { + Ok(uid) => libc::getpwuid(uid), + Err(..) => { + let uname = CString::new(uname).expect("username"); + libc::getpwnam(uname.as_ptr()) + } + }; + + if pwd.is_null() { + warn!("user {} not found", uname); + return; + } + + let pwd = &*pwd; + + // setgid first, because we may not allowed to do it anymore after setuid + if libc::setgid(pwd.pw_gid as libc::gid_t) != 0 { + let err = Error::last_os_error(); + + warn!( + "could not change group id to user {:?}'s gid: {}, uid: {}, error: {}", + CStr::from_ptr(pwd.pw_name), + pwd.pw_gid, + pwd.pw_uid, + err + ); + return; + } + + if libc::initgroups(pwd.pw_name, pwd.pw_gid.try_into().unwrap()) != 0 { + let err = Error::last_os_error(); + warn!( + "could not change supplementary groups to user {:?}'s gid: {}, uid: {}, error: {}", + CStr::from_ptr(pwd.pw_name), + pwd.pw_gid, + pwd.pw_uid, + err + ); + return; + } + + if libc::setuid(pwd.pw_uid) != 0 { + let err = Error::last_os_error(); + warn!( + "could not change user id to user {:?}'s gid: {}, uid: {}, error: {}", + CStr::from_ptr(pwd.pw_name), + pwd.pw_gid, + pwd.pw_uid, + err + ); + return; + } + } +} + +/// Check if running from a root user +#[inline(always)] +pub fn check_run_from_root() { + #[cfg(unix)] + unsafe { + use log::warn; + + if libc::geteuid() == 0 { + warn!("running from root user"); + } + } +}