diff --git a/.env.example b/.env.example index 4cc1a33..ac3855b 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ TAG=latest # set timezone to get correct log timestamp TZ=ETC/UTC +# container uid and gid (must match the owner of mounted directories) +GODOXY_UID=1000 +GODOXY_GID=1000 + # API JWT Configuration (common) # generate secret with `openssl rand -base64 32` GODOXY_API_JWT_SECRET= @@ -44,9 +48,19 @@ GODOXY_API_PASSWORD=password GODOXY_HTTP_ADDR=:80 GODOXY_HTTPS_ADDR=:443 +# Enable HTTP3 +GODOXY_HTTP3_ENABLED=true + # API listening address GODOXY_API_ADDR=127.0.0.1:8888 +# Metrics +GODOXY_METRICS_DISABLE_CPU=false +GODOXY_METRICS_DISABLE_MEMORY=false +GODOXY_METRICS_DISABLE_DISK=false +GODOXY_METRICS_DISABLE_NETWORK=false +GODOXY_METRICS_DISABLE_SENSORS=false + # Frontend listening port GODOXY_FRONTEND_PORT=3000 @@ -56,6 +70,7 @@ GODOXY_FRONTEND_ALIASES=godoxy # Docker socket # /var/run/podman/podman.sock for podman DOCKER_SOCKET=/var/run/docker.sock +SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375 # Debug mode GODOXY_DEBUG=false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b44aae4..12be8e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,16 @@ HEALTHCHECK NONE # trunk-ignore(hadolint/DL3018) RUN apk add --no-cache tzdata make libcap-setcap +ENV GOPATH=/root/go + +WORKDIR /src + +COPY go.mod go.sum ./ +COPY agent ./agent +COPY internal/dnsproviders ./internal/dnsproviders + +RUN go mod download -x + # Stage 2: builder FROM deps AS builder @@ -17,12 +27,6 @@ COPY internal ./internal COPY pkg ./pkg COPY agent ./agent -# Only copy go.mod and go.sum initially for better caching -COPY go.mod go.sum /src/ - -ENV GOPATH=/root/go -RUN go mod download -x - ARG VERSION ENV VERSION=${VERSION} @@ -31,9 +35,8 @@ ENV MAKE_ARGS=${MAKE_ARGS} ENV GOCACHE=/root/.cache/go-build ENV GOPATH=/root/go -RUN make ${MAKE_ARGS} build link-binary && \ - mv bin /app/ && \ - mkdir -p /app/error_pages /app/certs + +RUN make ${MAKE_ARGS} docker=1 build # Stage 3: Final image FROM scratch @@ -45,10 +48,7 @@ LABEL proxy.exclude=1 COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo # copy binary -COPY --from=builder /app /app - -# copy example config -COPY config.example.yml /app/config/config.yml +COPY --from=builder /app/run /app/run # copy certs COPY --from=builder /etc/ssl/certs /etc/ssl/certs diff --git a/Makefile b/Makefile index c866a81..52304fb 100755 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +shell := /bin/sh export VERSION ?= $(shell git describe --tags --abbrev=0) export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M') export GOOS = linux @@ -59,20 +60,29 @@ else SETCAP_CMD = sudo setcap endif + +# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443 +POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH}; +ifeq ($(docker), 1) + POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run; +endif + .PHONY: debug test: GODOXY_TEST=1 go test ./internal/... +docker-build-test: + docker build -t godoxy . + docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent . + get: for dir in ${PWD} ${PWD}/agent; do cd $$dir && go get -u ./... && go mod tidy; done build: - mkdir -p bin + mkdir -p $(shell dirname ${BIN_PATH}) cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ${CMD_PATH} - - # CAP_NET_BIND_SERVICE: permission for binding to :80 and :443 - $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH} + ${POST_BUILD} run: [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH} @@ -82,7 +92,7 @@ debug: sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test' mtrace: - bin/godoxy debug-ls-mtrace > mtrace.json + ${BIN_PATH} debug-ls-mtrace > mtrace.json rapid-crash: docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\ @@ -99,8 +109,5 @@ ci-test: cloc: cloc --not-match-f '_test.go$$' cmd internal pkg -link-binary: - ln -s /app/${NAME} bin/run - push-github: git push origin $(shell git rev-parse --abbrev-ref HEAD) \ No newline at end of file diff --git a/compose.example.yml b/compose.example.yml index acb49e3..f27f6f0 100755 --- a/compose.example.yml +++ b/compose.example.yml @@ -1,17 +1,47 @@ --- services: + socket-proxy: + container_name: socket-proxy + image: lscr.io/linuxserver/socket-proxy:latest + environment: + - ALLOW_START=1 + - ALLOW_STOP=1 + - ALLOW_RESTARTS=1 + - CONTAINERS=1 + - EVENTS=1 + - PING=1 + - POST=1 + - VERSION=1 + volumes: + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + restart: unless-stopped + tmpfs: + - /run + ports: + - ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:2375"] + interval: 1m30s + timeout: 30s + retries: 5 + start_period: 30s frontend: image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest} container_name: godoxy-frontend restart: unless-stopped network_mode: host # do not change this env_file: .env + user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000} + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - all depends_on: - app environment: + HOSTNAME: 127.0.0.1 PORT: ${GODOXY_FRONTEND_PORT:-3000} - - # modify below to fit your needs labels: proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy} proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000} @@ -29,11 +59,19 @@ services: restart: always network_mode: host # do not change this env_file: .env + user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000} + security_opt: + - no-new-privileges:true + cap_drop: + - all + cap_add: + - NET_BIND_SERVICE + environment: + - DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375} volumes: - - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock - ./config:/app/config - ./logs:/app/logs - - ./error_pages:/app/error_pages + - ./error_pages:/app/error_pages:ro - ./data:/app/data # To use autocert, certs will be stored in "./certs". diff --git a/scripts/setup.sh b/scripts/setup.sh index b6e2853..dbbc451 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -2,6 +2,33 @@ set -e # Exit on error +check_cmd() { + not_available=() + for cmd in "$@"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + not_available+=("$cmd") + fi + done + if [ "${#not_available[@]}" -gt 0 ]; then + echo "Error: ${not_available[*]} unavailable, please install it first" + exit 1 + fi +} + +check_cmd openssl docker + +# quit if running user is root +if [ "$EUID" -eq 0 ]; then + echo "Error: Please do not run this script as root" + exit 1 +fi + +# check if user has docker permission +if [ -z "$(docker ps >/dev/null 2>&1)" ]; then + echo "Error: User $USER does not have permission to run docker, please add it to docker group" + exit 1 +fi + # Detect download tool if command -v curl >/dev/null 2>&1; then DOWNLOAD_TOOL="curl" @@ -10,13 +37,8 @@ elif command -v wget >/dev/null 2>&1; then DOWNLOAD_TOOL="wget" DOWNLOAD_CMD="wget -qO" else - read -p "Neither curl nor wget is installed, install curl? (y/n): " INSTALL - if [ "$INSTALL" == "y" ]; then - install_pkg "curl" - else - echo "Error: Neither curl nor wget is installed. Please install one of them and try again." - exit 1 - fi + echo "Error: Neither curl nor wget is installed. Please install one of them and try again." + exit 1 fi echo "Using ${DOWNLOAD_TOOL} for downloads" @@ -36,43 +58,12 @@ COMPOSE_FILE_NAME="compose.yml" COMPOSE_EXAMPLE_FILE_NAME="compose.example.yml" CONFIG_FILE_NAME="config.yml" CONFIG_EXAMPLE_FILE_NAME="config.example.yml" +CONFIG_FILE_PATH="${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}" +REQUIRED_DIRECTORIES=("config" "logs" "error_pages" "data" "certs") echo "Setting up GoDoxy" echo "Branch: ${BRANCH}" -install_pkg() { - # detect package manager - if command -v apt >/dev/null 2>&1; then - apt install -y "$1" - elif command -v yum >/dev/null 2>&1; then - yum install -y "$1" - elif command -v pacman >/dev/null 2>&1; then - pacman -S --noconfirm "$1" - else - echo "Error: No supported package manager found" - exit 1 - fi -} - -check_pkg() { - local cmd="$1" - local pkg="$2" - if ! command -v "$cmd" >/dev/null 2>&1; then - # check if user is root - if [ "$EUID" -ne 0 ]; then - echo "Error: $pkg is not installed and you are not running as root. Please install it and try again." - exit 1 - fi - read -p "$pkg is not installed, install it? (y/n): " INSTALL - if [ "$INSTALL" == "y" ]; then - install_pkg "$pkg" - else - echo "Error: $pkg is not installed. Please install it and try again." - exit 1 - fi - fi -} - # Function to check if file/directory exists has_file_or_dir() { [ -e "$1" ] @@ -133,6 +124,32 @@ ask_while_empty() { eval "$var_name=\"$value\"" } +ask_multiple_choice() { + local var_name="$1" + local prompt="$2" + shift 2 + local choices=("$@") + local n_choices="${#choices[@]}" + local value="" + local valid=0 + while [ $valid -eq 0 ]; do + echo -e "$prompt" + for i in "${!choices[@]}"; do + echo "$((i + 1)). ${choices[$i]}" + done + read -p "Enter your choice: " value + if [ -z "$value" ]; then + echo "Error: $var_name cannot be empty, please try again" + fi + if [ "$value" -gt "$n_choices" ] || [ "$value" -lt 1 ]; then + echo "Error: invalid choice, please try again" + else + valid=1 + fi + done + eval "$var_name=\"${choices[$((value - 1))]}\"" +} + get_timezone() { if [ -f /etc/timezone ]; then TIMEZONE=$(cat /etc/timezone) @@ -142,38 +159,45 @@ get_timezone() { elif command -v timedatectl >/dev/null 2>&1; then TIMEZONE=$(timedatectl status | grep "Time zone" | awk '{print $3}') if [ -n "$TIMEZONE" ]; then - echo "$TIMEZONE" + echo "Detected timezone: $TIMEZONE" fi else echo "Warning: could not detect timezone, you may set it manually in ${DOT_ENV_PATH} to have correct time in logs" fi } -check_pkg "openssl" "openssl" -check_pkg "docker" "docker-ce" +setenv() { + local key="$1" + local value="$2" + # uncomment line if it is commented + sed -i "/^# *${key}=/s/^# *//" "$DOT_ENV_PATH" + sed -i "s|${key}=.*|${key}=\"${value}\"|" "$DOT_ENV_PATH" + echo "${key}=${value}" +} # Setup required configurations -# 1. Config base directory -mkdir_if_not_exists "$CONFIG_BASE_PATH" +# 1. Setup required directories +for dir in "${REQUIRED_DIRECTORIES[@]}"; do + mkdir_if_not_exists "$dir" +done # 2. .env file fetch_file "$DOT_ENV_EXAMPLE_PATH" "$DOT_ENV_PATH" # set random JWT secret -JWT_SECRET=$(openssl rand -base64 32) -sed -i "s|GODOXY_API_JWT_SECRET=.*|GODOXY_API_JWT_SECRET=${JWT_SECRET}|" "$DOT_ENV_PATH" +setenv "GODOXY_API_JWT_SECRET" "$(openssl rand -base64 32)" # set timezone get_timezone if [ -n "$TIMEZONE" ]; then - sed -i "s|TZ=.*|TZ=${TIMEZONE}|" "$DOT_ENV_PATH" + setenv "TZ" "$TIMEZONE" fi # 3. docker-compose.yml fetch_file "$COMPOSE_EXAMPLE_FILE_NAME" "$COMPOSE_FILE_NAME" # 4. config.yml -fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}" +fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "$CONFIG_FILE_PATH" # 5. setup authentication @@ -182,44 +206,71 @@ echo "Setting up login user" ask_while_empty "Enter login username: " LOGIN_USERNAME ask_while_empty "Enter login password: " LOGIN_PASSWORD echo "Setting up login user \"$LOGIN_USERNAME\" with password \"$LOGIN_PASSWORD\"" -sed -i "s|GODOXY_API_USERNAME=.*|GODOXY_API_USERNAME=${LOGIN_USERNAME}|" "$DOT_ENV_PATH" -sed -i "s|GODOXY_API_PASSWORD=.*|GODOXY_API_PASSWORD=${LOGIN_PASSWORD}|" "$DOT_ENV_PATH" +setenv "GODOXY_API_USERNAME" "$LOGIN_USERNAME" +setenv "GODOXY_API_PASSWORD" "$LOGIN_PASSWORD" # 6. setup autocert -# ask if want to enable autocert -echo "Setting up autocert for SSL certificate" -ask_while_empty "Do you want to enable autocert? (y/n): " ENABLE_AUTOCERT - # quit if not using autocert if [ "$ENABLE_AUTOCERT" == "y" ]; then # ask for domain echo "Setting up autocert" - ask_while_empty "Enter domain (e.g. example.com): " DOMAIN + skip=false # ask for email ask_while_empty "Enter email for Let's Encrypt: " EMAIL - # ask if using cloudflare - ask_while_empty "Is cloudflare the current DNS nameserver? (y/n): " USE_CLOUDFLARE + # select dns provider + ask_multiple_choice DNS_PROVIDER "Select DNS provider:" \ + "Cloudflare" \ + "CloudDNS" \ + "DuckDNS" \ + "Other" - # ask for cloudflare api key - if [ "$USE_CLOUDFLARE" = "y" ]; then - ask_while_empty "Enter cloudflare zone api key: " CLOUDFLARE_API_KEY - cat <>"$CONFIG_BASE_PATH/$CONFIG_FILE_NAME" -autocert: - provider: cloudflare - email: $EMAIL - domains: - - "*.${DOMAIN}" - - "${DOMAIN}" - options: - auth_token: "$CLOUDFLARE_API_KEY" -EOF + # ask for dns provider credentials + if [ "$DNS_PROVIDER" == "Cloudflare" ]; then + provider="cloudflare" + read -p "Enter cloudflare zone api key: " auth_token + options=("auth_token: \"$auth_token\"") + elif [ "$DNS_PROVIDER" == "CloudDNS" ]; then + provider="clouddns" + read -p "Enter clouddns client_id: " client_id + read -p "Enter clouddns email: " email + read -p "Enter clouddns password: " password + options=( + "client_id: \"$client_id\"" + "email: \"$email\"" + "password: \"$password\"" + ) + elif [ "$DNS_PROVIDER" == "DuckDNS" ]; then + provider="duckdns" + read -p "Enter duckdns token: " token + options=("token: \"$token\"") else - echo "Not using cloudflare, skipping autocert setup" - echo "Please refer to ${WIKI_URL}/Supported-DNS-01-Providers for more information" + echo "Please check Wiki for other DNS providers: ${WIKI_URL}/Supported-DNS%E2%80%9001-Providers" + echo "Skipping autocert setup" + skip=true + fi + if [ "$skip" == false ]; then + autocert_config=" +autocert: + provider: \"${provider}\" + email: \"${EMAIL}\" + domains: + - \"*.${BASE_DOMAIN}\" + - \"${BASE_DOMAIN}\" + options: +" + for option in "${options[@]}"; do + autocert_config+=" ${option}\n" + done + autocert_config+="\n" + echo -e "${autocert_config}$(<"$CONFIG_FILE_PATH")" >"$CONFIG_FILE_PATH" fi fi +# 7. set uid and gid +setenv "GODOXY_UID" "$(id -u)" +setenv "GODOXY_GID" "$(id -g)" + echo "Setup finished"