From 262c4d3d81ec8136bcfcb9fa8d5cf8dd380dc019 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:35:46 +0800 Subject: [PATCH] fix(microwarp): verify WARP IP rotation Add the verified MicroWARP packages to the localApps branch, including real SOCKS egress verification, retry and rollback behavior, and upgrade-safe replacement of legacy root rotation scripts while preserving user data. --- apps/microwarp/0.1.3-rotator-http/.env.sample | 18 ++ apps/microwarp/0.1.3-rotator-http/data.yml | 277 ++++++++++++++++ .../0.1.3-rotator-http/docker-compose.yml | 58 ++++ apps/microwarp/0.1.3-rotator-http/rotate.sh | 300 ++++++++++++++++++ .../0.1.3-rotator-http/scripts/init.sh | 38 +++ .../0.1.3-rotator-http/scripts/uninstall.sh | 2 + .../0.1.3-rotator-http/scripts/upgrade.sh | 112 +++++++ apps/microwarp/0.1.3-rotator/.env.sample | 17 + apps/microwarp/0.1.3-rotator/data.yml | 260 +++++++++++++++ .../0.1.3-rotator/docker-compose.yml | 40 +++ apps/microwarp/0.1.3-rotator/rotate.sh | 300 ++++++++++++++++++ apps/microwarp/0.1.3-rotator/scripts/init.sh | 38 +++ .../0.1.3-rotator/scripts/uninstall.sh | 2 + .../0.1.3-rotator/scripts/upgrade.sh | 112 +++++++ apps/microwarp/README.md | 107 +++++++ apps/microwarp/data.yml | 32 ++ apps/microwarp/http-latest/.env.sample | 13 + apps/microwarp/http-latest/data.yml | 197 ++++++++++++ apps/microwarp/http-latest/docker-compose.yml | 51 +++ apps/microwarp/http-latest/scripts/init.sh | 38 +++ .../http-latest/scripts/uninstall.sh | 2 + apps/microwarp/http-latest/scripts/upgrade.sh | 39 +++ apps/microwarp/latest/.env.sample | 12 + apps/microwarp/latest/data.yml | 180 +++++++++++ apps/microwarp/latest/docker-compose.yml | 33 ++ apps/microwarp/latest/scripts/init.sh | 38 +++ apps/microwarp/latest/scripts/uninstall.sh | 2 + apps/microwarp/latest/scripts/upgrade.sh | 39 +++ apps/microwarp/logo.png | Bin 0 -> 4768 bytes .../microwarp/rotator-http-latest/.env.sample | 18 ++ apps/microwarp/rotator-http-latest/data.yml | 277 ++++++++++++++++ .../rotator-http-latest/docker-compose.yml | 58 ++++ apps/microwarp/rotator-http-latest/rotate.sh | 300 ++++++++++++++++++ .../rotator-http-latest/scripts/init.sh | 38 +++ .../rotator-http-latest/scripts/uninstall.sh | 2 + .../rotator-http-latest/scripts/upgrade.sh | 112 +++++++ apps/microwarp/rotator-latest/.env.sample | 17 + apps/microwarp/rotator-latest/data.yml | 260 +++++++++++++++ .../rotator-latest/docker-compose.yml | 40 +++ apps/microwarp/rotator-latest/rotate.sh | 300 ++++++++++++++++++ apps/microwarp/rotator-latest/scripts/init.sh | 38 +++ .../rotator-latest/scripts/uninstall.sh | 2 + .../rotator-latest/scripts/upgrade.sh | 112 +++++++ 43 files changed, 3931 insertions(+) create mode 100644 apps/microwarp/0.1.3-rotator-http/.env.sample create mode 100644 apps/microwarp/0.1.3-rotator-http/data.yml create mode 100644 apps/microwarp/0.1.3-rotator-http/docker-compose.yml create mode 100644 apps/microwarp/0.1.3-rotator-http/rotate.sh create mode 100644 apps/microwarp/0.1.3-rotator-http/scripts/init.sh create mode 100755 apps/microwarp/0.1.3-rotator-http/scripts/uninstall.sh create mode 100644 apps/microwarp/0.1.3-rotator-http/scripts/upgrade.sh create mode 100644 apps/microwarp/0.1.3-rotator/.env.sample create mode 100644 apps/microwarp/0.1.3-rotator/data.yml create mode 100644 apps/microwarp/0.1.3-rotator/docker-compose.yml create mode 100644 apps/microwarp/0.1.3-rotator/rotate.sh create mode 100644 apps/microwarp/0.1.3-rotator/scripts/init.sh create mode 100755 apps/microwarp/0.1.3-rotator/scripts/uninstall.sh create mode 100644 apps/microwarp/0.1.3-rotator/scripts/upgrade.sh create mode 100644 apps/microwarp/README.md create mode 100644 apps/microwarp/data.yml create mode 100644 apps/microwarp/http-latest/.env.sample create mode 100644 apps/microwarp/http-latest/data.yml create mode 100644 apps/microwarp/http-latest/docker-compose.yml create mode 100644 apps/microwarp/http-latest/scripts/init.sh create mode 100755 apps/microwarp/http-latest/scripts/uninstall.sh create mode 100644 apps/microwarp/http-latest/scripts/upgrade.sh create mode 100644 apps/microwarp/latest/.env.sample create mode 100644 apps/microwarp/latest/data.yml create mode 100644 apps/microwarp/latest/docker-compose.yml create mode 100644 apps/microwarp/latest/scripts/init.sh create mode 100755 apps/microwarp/latest/scripts/uninstall.sh create mode 100644 apps/microwarp/latest/scripts/upgrade.sh create mode 100644 apps/microwarp/logo.png create mode 100644 apps/microwarp/rotator-http-latest/.env.sample create mode 100644 apps/microwarp/rotator-http-latest/data.yml create mode 100644 apps/microwarp/rotator-http-latest/docker-compose.yml create mode 100644 apps/microwarp/rotator-http-latest/rotate.sh create mode 100644 apps/microwarp/rotator-http-latest/scripts/init.sh create mode 100755 apps/microwarp/rotator-http-latest/scripts/uninstall.sh create mode 100644 apps/microwarp/rotator-http-latest/scripts/upgrade.sh create mode 100644 apps/microwarp/rotator-latest/.env.sample create mode 100644 apps/microwarp/rotator-latest/data.yml create mode 100644 apps/microwarp/rotator-latest/docker-compose.yml create mode 100644 apps/microwarp/rotator-latest/rotate.sh create mode 100644 apps/microwarp/rotator-latest/scripts/init.sh create mode 100755 apps/microwarp/rotator-latest/scripts/uninstall.sh create mode 100644 apps/microwarp/rotator-latest/scripts/upgrade.sh diff --git a/apps/microwarp/0.1.3-rotator-http/.env.sample b/apps/microwarp/0.1.3-rotator-http/.env.sample new file mode 100644 index 000000000..16010ebc4 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/.env.sample @@ -0,0 +1,18 @@ +CONTAINER_NAME=microwarp +PANEL_APP_PORT_SOCKS5=1080 +PANEL_APP_PORT_HTTP_PROXY=8080 +BIND_PORT=1080 +ROTATE_INTERVAL_MINUTES=0 +ROTATE_MAX_ATTEMPTS=5 +ROTATE_RETRY_DELAY_SECONDS=5 +TEST_URL=https://cloudflare.com/cdn-cgi/trace +IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace +SOCKS_USER= +SOCKS_PASS= +ENDPOINT_IP= +GH_PROXY= +TAILSCALE_CIDR=100.64.0.0/10 +MTU=1280 +WARP_WGCF_CONF= +TZ=Asia/Shanghai +APP_DATA_DIR_1=./data diff --git a/apps/microwarp/0.1.3-rotator-http/data.yml b/apps/microwarp/0.1.3-rotator-http/data.yml new file mode 100644 index 000000000..95c8f55df --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/data.yml @@ -0,0 +1,277 @@ +additionalProperties: + formFields: + - default: 1080 + envKey: PANEL_APP_PORT_SOCKS5 + labelEn: SOCKS5 Port + labelZh: SOCKS5 端口 + label: + en: SOCKS5 Port + zh: SOCKS5 端口 + zh-Hant: SOCKS5 連接埠 + ja: SOCKS5 ポート + ko: SOCKS5 포트 + ru: Порт SOCKS5 + ms: Port SOCKS5 + pt-br: Porta SOCKS5 + required: true + rule: paramPort + type: number + edit: true + - default: 8080 + envKey: PANEL_APP_PORT_HTTP_PROXY + labelEn: HTTP Proxy Port + labelZh: HTTP 代理端口 + label: + en: HTTP Proxy Port + zh: HTTP 代理端口 + zh-Hant: HTTP 代理連接埠 + ja: HTTP プロキシポート + ko: HTTP 프록시 포트 + ru: Порт HTTP-прокси + ms: Port proksi HTTP + pt-br: Porta do proxy HTTP + required: true + rule: paramPort + type: number + edit: true + - default: 1080 + envKey: BIND_PORT + labelEn: Internal Bind Port + labelZh: 容器内部绑定端口 + label: + en: Internal Bind Port + zh: 容器内部绑定端口 + zh-Hant: 容器內部綁定連接埠 + ja: コンテナ内部バインドポート + ko: 컨테이너 내부 바인드 포트 + ru: Внутренний порт привязки контейнера + ms: Port bind dalaman kontena + pt-br: Porta interna de bind do contêiner + required: true + rule: paramPort + type: number + edit: true + - default: 0 + envKey: ROTATE_INTERVAL_MINUTES + labelEn: Rotate Interval Minutes + labelZh: 轮换间隔(分钟) + label: + en: Rotate Interval Minutes + zh: 轮换间隔(分钟) + zh-Hant: 輪換間隔(分鐘) + ja: ローテーション間隔(分) + ko: 로테이션 간격(분) + ru: Интервал ротации (минуты) + ms: Selang putaran (minit) + pt-br: Intervalo de rotação (minutos) + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_MAX_ATTEMPTS + labelEn: Rotation Attempts + labelZh: 轮换尝试次数 + label: + en: Rotation Attempts + zh: 轮换尝试次数 + zh-Hant: 輪換嘗試次數 + ja: ローテーション試行回数 + ko: 로테이션 시도 횟수 + ru: Количество попыток ротации + ms: Bilangan percubaan putaran + pt-br: Número de tentativas de rotação + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_RETRY_DELAY_SECONDS + labelEn: Rotation Retry Delay Seconds + labelZh: 轮换重试间隔(秒) + label: + en: Rotation Retry Delay Seconds + zh: 轮换重试间隔(秒) + zh-Hant: 輪換重試間隔(秒) + ja: ローテーション再試行間隔(秒) + ko: 로테이션 재시도 간격(초) + ru: Задержка между попытками ротации (секунды) + ms: Kelewatan percubaan semula putaran (saat) + pt-br: Atraso entre tentativas de rotação (segundos) + required: true + type: number + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: TEST_URL + labelEn: Probe URL + labelZh: 探测 URL + label: + en: Probe URL + zh: 探测 URL + zh-Hant: 探測 URL + ja: プローブ URL + ko: 점검 URL + ru: URL проверки + ms: URL ujian + pt-br: URL de verificação + required: true + type: text + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: IP_CHECK_URL + labelEn: Egress IP Check URL + labelZh: 出口 IP 检查 URL + label: + en: Egress IP Check URL + zh: 出口 IP 检查 URL + zh-Hant: 出口 IP 檢查 URL + ja: 出口 IP 確認 URL + ko: 출구 IP 확인 URL + ru: URL проверки исходного IP + ms: URL semakan IP keluar + pt-br: URL de verificação do IP de saída + required: true + type: text + edit: true + - default: "" + envKey: SOCKS_USER + labelEn: SOCKS Username + labelZh: SOCKS 用户名 + label: + en: SOCKS Username + zh: SOCKS 用户名 + zh-Hant: SOCKS 使用者名稱 + ja: SOCKS ユーザー名 + ko: SOCKS 사용자명 + ru: Имя пользователя SOCKS + ms: Nama pengguna SOCKS + pt-br: Usuário SOCKS + required: false + type: text + edit: true + - default: "" + envKey: SOCKS_PASS + labelEn: SOCKS Password + labelZh: SOCKS 密码 + label: + en: SOCKS Password + zh: SOCKS 密码 + zh-Hant: SOCKS 密碼 + ja: SOCKS パスワード + ko: SOCKS 비밀번호 + ru: Пароль SOCKS + ms: Kata laluan SOCKS + pt-br: Senha SOCKS + required: false + type: text + edit: true + - default: "" + envKey: ENDPOINT_IP + labelEn: WARP Endpoint + labelZh: WARP 终结点 + label: + en: WARP Endpoint + zh: WARP 终结点 + zh-Hant: WARP 終結點 + ja: WARP エンドポイント + ko: WARP 엔드포인트 + ru: WARP endpoint + ms: Titik akhir WARP + pt-br: Endpoint WARP + required: false + type: text + edit: true + - default: "" + envKey: GH_PROXY + labelEn: wgcf Download Proxy + labelZh: wgcf 下载代理 + label: + en: wgcf Download Proxy + zh: wgcf 下载代理 + zh-Hant: wgcf 下載代理 + ja: wgcf ダウンロードプロキシ + ko: wgcf 다운로드 프록시 + ru: Прокси для загрузки wgcf + ms: Proksi muat turun wgcf + pt-br: Proxy de download do wgcf + required: false + type: text + edit: true + - default: 100.64.0.0/10 + envKey: TAILSCALE_CIDR + labelEn: Tailscale Return Route CIDR + labelZh: Tailscale 回程路由 CIDR + label: + en: Tailscale Return Route CIDR + zh: Tailscale 回程路由 CIDR + zh-Hant: Tailscale 回程路由 CIDR + ja: Tailscale 戻りルート CIDR + ko: Tailscale 리턴 라우트 CIDR + ru: CIDR обратного маршрута Tailscale + ms: CIDR laluan balik Tailscale + pt-br: CIDR de rota de retorno do Tailscale + required: true + type: text + edit: true + - default: 1280 + envKey: MTU + labelEn: WireGuard MTU + labelZh: WireGuard MTU + label: + en: WireGuard MTU + zh: WireGuard MTU + zh-Hant: WireGuard MTU + ja: WireGuard MTU + ko: WireGuard MTU + ru: MTU WireGuard + ms: MTU WireGuard + pt-br: MTU do WireGuard + required: true + type: number + edit: true + - default: "" + envKey: WARP_WGCF_CONF + labelEn: WireGuard Config Content + labelZh: WireGuard 配置内容 + label: + en: WireGuard Config Content + zh: WireGuard 配置内容 + zh-Hant: WireGuard 設定內容 + ja: WireGuard 設定内容 + ko: WireGuard 구성 내용 + ru: Содержимое конфигурации WireGuard + ms: Kandungan konfigurasi WireGuard + pt-br: Conteúdo da configuração WireGuard + required: false + type: text + edit: true + - default: Asia/Shanghai + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text + edit: true + - default: ./data + envKey: APP_DATA_DIR_1 + labelEn: WireGuard Data Directory + labelZh: WireGuard 数据目录 + label: + en: WireGuard Data Directory + zh: WireGuard 数据目录 + zh-Hant: WireGuard 資料目錄 + ja: WireGuard データディレクトリ + ko: WireGuard 데이터 디렉터리 + ru: Каталог данных WireGuard + ms: Direktori data WireGuard + pt-br: Diretório de dados do WireGuard + required: true + type: text + edit: true diff --git a/apps/microwarp/0.1.3-rotator-http/docker-compose.yml b/apps/microwarp/0.1.3-rotator-http/docker-compose.yml new file mode 100644 index 000000000..42ad93024 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/docker-compose.yml @@ -0,0 +1,58 @@ +services: + microwarp: + image: "ghcr.io/ccbkkb/microwarp:0.1.3" + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + ports: + - "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}" + environment: + - TZ=${TZ} + - BIND_ADDR=0.0.0.0 + - BIND_PORT=${BIND_PORT} + - ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0} + - ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5} + - ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5} + - TEST_URL=${TEST_URL} + - IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace} + - SOCKS_USER=${SOCKS_USER:-} + - SOCKS_PASS=${SOCKS_PASS:-} + - ENDPOINT_IP=${ENDPOINT_IP:-} + - GH_PROXY=${GH_PROXY:-} + - TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10} + - MTU=${MTU} + - WARP_WGCF_CONF=${WARP_WGCF_CONF:-} + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + volumes: + - "${APP_DATA_DIR_1}:/etc/wireguard" + - "./rotate.sh:/usr/local/bin/rotate.sh:ro" + restart: always + command: ["/bin/sh", "/usr/local/bin/rotate.sh"] + networks: + - 1panel-network + + http-proxy: + image: "gogost/gost:latest" + container_name: ${CONTAINER_NAME}-http + labels: + createdBy: "Apps" + depends_on: + - microwarp + command: + - "-L" + - "http://0.0.0.0:8080" + - "-F" + - "socks5://microwarp:${BIND_PORT}" + ports: + - "${PANEL_APP_PORT_HTTP_PROXY}:8080" + restart: always + networks: + - 1panel-network + +networks: + 1panel-network: + external: true diff --git a/apps/microwarp/0.1.3-rotator-http/rotate.sh b/apps/microwarp/0.1.3-rotator-http/rotate.sh new file mode 100644 index 000000000..c3b9dbe3d --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/rotate.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env sh +set -eu + +INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}" +ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}" +ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}" +TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}" +IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}" +SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}" +SOCKS_USER_VALUE="${SOCKS_USER:-}" +SOCKS_PASS_VALUE="${SOCKS_PASS:-}" +WG_DIR="/etc/wireguard" +WG_CONF="${WG_DIR}/wg0.conf" +PREVIOUS_CONF="${WG_CONF}.rotation-previous" +FIXED_CONFIG="${WARP_WGCF_CONF:-}" +LOG_PREFIX="[microwarp]" +ENTRY_PID="" +CURRENT_EGRESS_IP="" + +log() { + echo "${LOG_PREFIX} $*" +} + +is_non_negative_int() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_positive_int() { + case "$1" in + ''|*[!0-9]*|0) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_ipv4() { + printf '%s\n' "$1" | awk -F. ' + NF == 4 { + for (i = 1; i <= 4; i++) { + if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1 + } + exit 0 + } + { exit 1 } + ' +} + +probe_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + curl_via_socks "$TEST_URL" >/dev/null 2>&1 +} + +curl_via_socks() { + url="$1" + if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \ + --proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url" + return $? + fi + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url" +} + +get_egress_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + + trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1 + current_ip="$(printf '%s\n' "$trace_output" | + awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')" + is_ipv4 "$current_ip" && printf '%s\n' "$current_ip" +} + +record_egress_ip() { + current_ip="$(get_egress_ip || true)" + if [ -n "$current_ip" ]; then + CURRENT_EGRESS_IP="$current_ip" + log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}" + else + CURRENT_EGRESS_IP="" + log "unable to verify current WARP egress IPv4" + fi + return 0 +} + +inject_fixed_config() { + if [ -z "$FIXED_CONFIG" ]; then + return 0 + fi + mkdir -p "$WG_DIR" + printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF" + chmod 600 "$WG_CONF" || true + log "wg0.conf injected from WARP_WGCF_CONF" +} + +wait_for_proxy() { + attempts=0 + while [ "$attempts" -lt 30 ]; do + if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then + wait "$ENTRY_PID" || true + ENTRY_PID="" + return 1 + fi + + if probe_ip; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 2 + done + return 1 +} + +stop_runtime() { + if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then + kill "$ENTRY_PID" 2>/dev/null || true + wait "$ENTRY_PID" || true + fi + ENTRY_PID="" +} + +teardown_for_rotation() { + stop_runtime + if ip link show wg0 >/dev/null 2>&1; then + wg-quick down wg0 >/dev/null 2>&1 || true + fi + rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env" +} + +start_runtime() { + CURRENT_EGRESS_IP="" + inject_fixed_config + log "starting MicroWARP runtime" + /app/entrypoint.sh & + ENTRY_PID=$! + if ! wait_for_proxy; then + log "runtime did not become ready in time" + stop_runtime + return 1 + fi + record_egress_ip + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "runtime exited while its egress was being checked" + stop_runtime + return 1 + fi + return 0 +} + +save_previous_config() { + if [ -s "$WG_CONF" ]; then + previous_tmp="${PREVIOUS_CONF}.tmp.$$" + rm -f "$previous_tmp" + if ! cp -p "$WG_CONF" "$previous_tmp"; then + rm -f "$previous_tmp" + return 1 + fi + if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then + rm -f "$previous_tmp" + return 1 + fi + return 0 + fi + return 1 +} + +restore_previous_runtime() { + if [ ! -s "$PREVIOUS_CONF" ]; then + return 1 + fi + + log "restoring the previous working WARP configuration" + teardown_for_rotation + if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then + log "unable to restore the previous WARP configuration" + return 1 + fi + start_runtime +} + +rotate_once() { + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "WARP runtime exited before the scheduled identity rebuild" + return 1 + fi + + record_egress_ip + old_ip="$CURRENT_EGRESS_IP" + if [ -z "$old_ip" ]; then + log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified" + return 0 + fi + attempt=1 + if ! save_previous_config; then + log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up" + return 0 + fi + + while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do + log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}" + teardown_for_rotation + + if start_runtime; then + new_ip="$CURRENT_EGRESS_IP" + if [ -n "$new_ip" ] && + [ "$new_ip" != "$old_ip" ]; then + rm -f "$PREVIOUS_CONF" + log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}" + return 0 + fi + + if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then + log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild" + else + log "WARP egress IPv4 could not be verified; retrying identity rebuild" + fi + else + log "WARP runtime failed during identity rebuild attempt ${attempt}" + fi + + if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then + sleep "$ROTATE_RETRY_DELAY_SECONDS" + fi + attempt=$((attempt + 1)) + done + + if restore_previous_runtime; then + log "no different public IPv4 was observed; previous runtime restored" + rm -f "$PREVIOUS_CONF" + return 0 + fi + + log "rotation failed and no working runtime is available" + return 1 +} + +on_signal() { + log "received stop signal" + stop_runtime + exit 0 +} + +main() { + interval="$INTERVAL_MINUTES" + if ! is_non_negative_int "$interval"; then + log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0" + interval=0 + fi + + if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then + log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5" + ROTATE_MAX_ATTEMPTS=5 + fi + + if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then + log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5" + ROTATE_RETRY_DELAY_SECONDS=5 + fi + + if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then + log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned" + interval=0 + fi + + trap on_signal INT TERM + + if ! start_runtime; then + exit 1 + fi + if [ "$interval" -eq 0 ]; then + log "scheduled rotation disabled" + wait "$ENTRY_PID" + exit $? + fi + + log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)" + while true; do + sleep "$((interval * 60))" + log "trigger scheduled WARP identity rebuild" + if ! rotate_once; then + log "rotation failed" + exit 1 + fi + done +} + +main "$@" diff --git a/apps/microwarp/0.1.3-rotator-http/scripts/init.sh b/apps/microwarp/0.1.3-rotator-http/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/0.1.3-rotator-http/scripts/uninstall.sh b/apps/microwarp/0.1.3-rotator-http/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/0.1.3-rotator-http/scripts/upgrade.sh b/apps/microwarp/0.1.3-rotator-http/scripts/upgrade.sh new file mode 100644 index 000000000..fe1e090e4 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator-http/scripts/upgrade.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" +TARGET_VERSION_DIR="0.1.3-rotator-http" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +resource_dir_for_app() { + local panel_root app_path app_key + + case "${APP_ROOT}" in + */apps/local/*) + panel_root="${APP_ROOT%%/apps/local/*}" + app_path="${APP_ROOT#"${panel_root}/apps/local/"}" + app_key="${app_path%%/*}" + [[ -n "${panel_root}" && -n "${app_key}" ]] || return 1 + printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}" + ;; + *) + return 1 + ;; + esac +} + +find_rotate_source() { + local resource_dir candidate_upgrade candidate_rotate + + if [[ -n "${ROTATE_SOURCE:-}" ]]; then + printf '%s\n' "${ROTATE_SOURCE}" + return 0 + fi + + if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then + candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh" + candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh" + if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \ + && cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then + printf '%s\n' "${candidate_rotate}" + return 0 + fi + fi + + if [[ -f "${APP_ROOT}/rotate.sh" ]] \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then + printf '%s\n' "${APP_ROOT}/rotate.sh" + return 0 + fi + + return 1 +} + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +rotate_target="${APP_ROOT}/rotate.sh" +rotate_source="$(find_rotate_source || true)" +if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then + echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2 + exit 1 +fi + +if [[ "${rotate_source}" != "${rotate_target}" ]] \ + && ! cmp -s "${rotate_source}" "${rotate_target}"; then + rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")" + # shellcheck disable=SC2317 + cleanup_rotate_tmp() { + rm -f -- "${rotate_tmp}" + } + trap cleanup_rotate_tmp EXIT + + cp -- "${rotate_source}" "${rotate_tmp}" + if [[ -e "${rotate_target}" ]]; then + chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true + else + chmod 0644 "${rotate_tmp}" + fi + mv -f -- "${rotate_tmp}" "${rotate_target}" + trap - EXIT + echo "Updated ${rotate_target} from the target version payload" +fi + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0 diff --git a/apps/microwarp/0.1.3-rotator/.env.sample b/apps/microwarp/0.1.3-rotator/.env.sample new file mode 100644 index 000000000..3c40a9f80 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/.env.sample @@ -0,0 +1,17 @@ +CONTAINER_NAME=microwarp +PANEL_APP_PORT_SOCKS5=1080 +BIND_PORT=1080 +ROTATE_INTERVAL_MINUTES=0 +ROTATE_MAX_ATTEMPTS=5 +ROTATE_RETRY_DELAY_SECONDS=5 +TEST_URL=https://cloudflare.com/cdn-cgi/trace +IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace +SOCKS_USER= +SOCKS_PASS= +ENDPOINT_IP= +GH_PROXY= +TAILSCALE_CIDR=100.64.0.0/10 +MTU=1280 +WARP_WGCF_CONF= +TZ=Asia/Shanghai +APP_DATA_DIR_1=./data diff --git a/apps/microwarp/0.1.3-rotator/data.yml b/apps/microwarp/0.1.3-rotator/data.yml new file mode 100644 index 000000000..fa0b013f3 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/data.yml @@ -0,0 +1,260 @@ +additionalProperties: + formFields: + - default: 1080 + envKey: PANEL_APP_PORT_SOCKS5 + labelEn: SOCKS5 Port + labelZh: SOCKS5 端口 + label: + en: SOCKS5 Port + zh: SOCKS5 端口 + zh-Hant: SOCKS5 連接埠 + ja: SOCKS5 ポート + ko: SOCKS5 포트 + ru: Порт SOCKS5 + ms: Port SOCKS5 + pt-br: Porta SOCKS5 + required: true + rule: paramPort + type: number + edit: true + - default: 1080 + envKey: BIND_PORT + labelEn: Internal Bind Port + labelZh: 容器内部绑定端口 + label: + en: Internal Bind Port + zh: 容器内部绑定端口 + zh-Hant: 容器內部綁定連接埠 + ja: コンテナ内部バインドポート + ko: 컨테이너 내부 바인드 포트 + ru: Внутренний порт привязки контейнера + ms: Port bind dalaman kontena + pt-br: Porta interna de bind do contêiner + required: true + rule: paramPort + type: number + edit: true + - default: 0 + envKey: ROTATE_INTERVAL_MINUTES + labelEn: Rotate Interval Minutes + labelZh: 轮换间隔(分钟) + label: + en: Rotate Interval Minutes + zh: 轮换间隔(分钟) + zh-Hant: 輪換間隔(分鐘) + ja: ローテーション間隔(分) + ko: 로테이션 간격(분) + ru: Интервал ротации (минуты) + ms: Selang putaran (minit) + pt-br: Intervalo de rotação (minutos) + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_MAX_ATTEMPTS + labelEn: Rotation Attempts + labelZh: 轮换尝试次数 + label: + en: Rotation Attempts + zh: 轮换尝试次数 + zh-Hant: 輪換嘗試次數 + ja: ローテーション試行回数 + ko: 로테이션 시도 횟수 + ru: Количество попыток ротации + ms: Bilangan percubaan putaran + pt-br: Número de tentativas de rotação + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_RETRY_DELAY_SECONDS + labelEn: Rotation Retry Delay Seconds + labelZh: 轮换重试间隔(秒) + label: + en: Rotation Retry Delay Seconds + zh: 轮换重试间隔(秒) + zh-Hant: 輪換重試間隔(秒) + ja: ローテーション再試行間隔(秒) + ko: 로테이션 재시도 간격(초) + ru: Задержка между попытками ротации (секунды) + ms: Kelewatan percubaan semula putaran (saat) + pt-br: Atraso entre tentativas de rotação (segundos) + required: true + type: number + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: TEST_URL + labelEn: Probe URL + labelZh: 探测 URL + label: + en: Probe URL + zh: 探测 URL + zh-Hant: 探測 URL + ja: プローブ URL + ko: 점검 URL + ru: URL проверки + ms: URL ujian + pt-br: URL de verificação + required: true + type: text + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: IP_CHECK_URL + labelEn: Egress IP Check URL + labelZh: 出口 IP 检查 URL + label: + en: Egress IP Check URL + zh: 出口 IP 检查 URL + zh-Hant: 出口 IP 檢查 URL + ja: 出口 IP 確認 URL + ko: 출구 IP 확인 URL + ru: URL проверки исходного IP + ms: URL semakan IP keluar + pt-br: URL de verificação do IP de saída + required: true + type: text + edit: true + - default: "" + envKey: SOCKS_USER + labelEn: SOCKS Username + labelZh: SOCKS 用户名 + label: + en: SOCKS Username + zh: SOCKS 用户名 + zh-Hant: SOCKS 使用者名稱 + ja: SOCKS ユーザー名 + ko: SOCKS 사용자명 + ru: Имя пользователя SOCKS + ms: Nama pengguna SOCKS + pt-br: Usuário SOCKS + required: false + type: text + edit: true + - default: "" + envKey: SOCKS_PASS + labelEn: SOCKS Password + labelZh: SOCKS 密码 + label: + en: SOCKS Password + zh: SOCKS 密码 + zh-Hant: SOCKS 密碼 + ja: SOCKS パスワード + ko: SOCKS 비밀번호 + ru: Пароль SOCKS + ms: Kata laluan SOCKS + pt-br: Senha SOCKS + required: false + type: text + edit: true + - default: "" + envKey: ENDPOINT_IP + labelEn: WARP Endpoint + labelZh: WARP 终结点 + label: + en: WARP Endpoint + zh: WARP 终结点 + zh-Hant: WARP 終結點 + ja: WARP エンドポイント + ko: WARP 엔드포인트 + ru: WARP endpoint + ms: Titik akhir WARP + pt-br: Endpoint WARP + required: false + type: text + edit: true + - default: "" + envKey: GH_PROXY + labelEn: wgcf Download Proxy + labelZh: wgcf 下载代理 + label: + en: wgcf Download Proxy + zh: wgcf 下载代理 + zh-Hant: wgcf 下載代理 + ja: wgcf ダウンロードプロキシ + ko: wgcf 다운로드 프록시 + ru: Прокси для загрузки wgcf + ms: Proksi muat turun wgcf + pt-br: Proxy de download do wgcf + required: false + type: text + edit: true + - default: 100.64.0.0/10 + envKey: TAILSCALE_CIDR + labelEn: Tailscale Return Route CIDR + labelZh: Tailscale 回程路由 CIDR + label: + en: Tailscale Return Route CIDR + zh: Tailscale 回程路由 CIDR + zh-Hant: Tailscale 回程路由 CIDR + ja: Tailscale 戻りルート CIDR + ko: Tailscale 리턴 라우트 CIDR + ru: CIDR обратного маршрута Tailscale + ms: CIDR laluan balik Tailscale + pt-br: CIDR de rota de retorno do Tailscale + required: true + type: text + edit: true + - default: 1280 + envKey: MTU + labelEn: WireGuard MTU + labelZh: WireGuard MTU + label: + en: WireGuard MTU + zh: WireGuard MTU + zh-Hant: WireGuard MTU + ja: WireGuard MTU + ko: WireGuard MTU + ru: MTU WireGuard + ms: MTU WireGuard + pt-br: MTU do WireGuard + required: true + type: number + edit: true + - default: "" + envKey: WARP_WGCF_CONF + labelEn: WireGuard Config Content + labelZh: WireGuard 配置内容 + label: + en: WireGuard Config Content + zh: WireGuard 配置内容 + zh-Hant: WireGuard 設定內容 + ja: WireGuard 設定内容 + ko: WireGuard 구성 내용 + ru: Содержимое конфигурации WireGuard + ms: Kandungan konfigurasi WireGuard + pt-br: Conteúdo da configuração WireGuard + required: false + type: text + edit: true + - default: Asia/Shanghai + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text + edit: true + - default: ./data + envKey: APP_DATA_DIR_1 + labelEn: WireGuard Data Directory + labelZh: WireGuard 数据目录 + label: + en: WireGuard Data Directory + zh: WireGuard 数据目录 + zh-Hant: WireGuard 資料目錄 + ja: WireGuard データディレクトリ + ko: WireGuard 데이터 디렉터리 + ru: Каталог данных WireGuard + ms: Direktori data WireGuard + pt-br: Diretório de dados do WireGuard + required: true + type: text + edit: true diff --git a/apps/microwarp/0.1.3-rotator/docker-compose.yml b/apps/microwarp/0.1.3-rotator/docker-compose.yml new file mode 100644 index 000000000..d54706150 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/docker-compose.yml @@ -0,0 +1,40 @@ +services: + microwarp: + image: "ghcr.io/ccbkkb/microwarp:0.1.3" + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + ports: + - "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}" + environment: + - TZ=${TZ} + - BIND_ADDR=0.0.0.0 + - BIND_PORT=${BIND_PORT} + - ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0} + - ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5} + - ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5} + - TEST_URL=${TEST_URL} + - IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace} + - SOCKS_USER=${SOCKS_USER:-} + - SOCKS_PASS=${SOCKS_PASS:-} + - ENDPOINT_IP=${ENDPOINT_IP:-} + - GH_PROXY=${GH_PROXY:-} + - TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10} + - MTU=${MTU} + - WARP_WGCF_CONF=${WARP_WGCF_CONF:-} + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + volumes: + - "${APP_DATA_DIR_1}:/etc/wireguard" + - "./rotate.sh:/usr/local/bin/rotate.sh:ro" + restart: always + command: ["/bin/sh", "/usr/local/bin/rotate.sh"] + networks: + - 1panel-network + +networks: + 1panel-network: + external: true diff --git a/apps/microwarp/0.1.3-rotator/rotate.sh b/apps/microwarp/0.1.3-rotator/rotate.sh new file mode 100644 index 000000000..c3b9dbe3d --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/rotate.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env sh +set -eu + +INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}" +ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}" +ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}" +TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}" +IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}" +SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}" +SOCKS_USER_VALUE="${SOCKS_USER:-}" +SOCKS_PASS_VALUE="${SOCKS_PASS:-}" +WG_DIR="/etc/wireguard" +WG_CONF="${WG_DIR}/wg0.conf" +PREVIOUS_CONF="${WG_CONF}.rotation-previous" +FIXED_CONFIG="${WARP_WGCF_CONF:-}" +LOG_PREFIX="[microwarp]" +ENTRY_PID="" +CURRENT_EGRESS_IP="" + +log() { + echo "${LOG_PREFIX} $*" +} + +is_non_negative_int() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_positive_int() { + case "$1" in + ''|*[!0-9]*|0) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_ipv4() { + printf '%s\n' "$1" | awk -F. ' + NF == 4 { + for (i = 1; i <= 4; i++) { + if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1 + } + exit 0 + } + { exit 1 } + ' +} + +probe_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + curl_via_socks "$TEST_URL" >/dev/null 2>&1 +} + +curl_via_socks() { + url="$1" + if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \ + --proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url" + return $? + fi + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url" +} + +get_egress_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + + trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1 + current_ip="$(printf '%s\n' "$trace_output" | + awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')" + is_ipv4 "$current_ip" && printf '%s\n' "$current_ip" +} + +record_egress_ip() { + current_ip="$(get_egress_ip || true)" + if [ -n "$current_ip" ]; then + CURRENT_EGRESS_IP="$current_ip" + log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}" + else + CURRENT_EGRESS_IP="" + log "unable to verify current WARP egress IPv4" + fi + return 0 +} + +inject_fixed_config() { + if [ -z "$FIXED_CONFIG" ]; then + return 0 + fi + mkdir -p "$WG_DIR" + printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF" + chmod 600 "$WG_CONF" || true + log "wg0.conf injected from WARP_WGCF_CONF" +} + +wait_for_proxy() { + attempts=0 + while [ "$attempts" -lt 30 ]; do + if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then + wait "$ENTRY_PID" || true + ENTRY_PID="" + return 1 + fi + + if probe_ip; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 2 + done + return 1 +} + +stop_runtime() { + if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then + kill "$ENTRY_PID" 2>/dev/null || true + wait "$ENTRY_PID" || true + fi + ENTRY_PID="" +} + +teardown_for_rotation() { + stop_runtime + if ip link show wg0 >/dev/null 2>&1; then + wg-quick down wg0 >/dev/null 2>&1 || true + fi + rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env" +} + +start_runtime() { + CURRENT_EGRESS_IP="" + inject_fixed_config + log "starting MicroWARP runtime" + /app/entrypoint.sh & + ENTRY_PID=$! + if ! wait_for_proxy; then + log "runtime did not become ready in time" + stop_runtime + return 1 + fi + record_egress_ip + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "runtime exited while its egress was being checked" + stop_runtime + return 1 + fi + return 0 +} + +save_previous_config() { + if [ -s "$WG_CONF" ]; then + previous_tmp="${PREVIOUS_CONF}.tmp.$$" + rm -f "$previous_tmp" + if ! cp -p "$WG_CONF" "$previous_tmp"; then + rm -f "$previous_tmp" + return 1 + fi + if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then + rm -f "$previous_tmp" + return 1 + fi + return 0 + fi + return 1 +} + +restore_previous_runtime() { + if [ ! -s "$PREVIOUS_CONF" ]; then + return 1 + fi + + log "restoring the previous working WARP configuration" + teardown_for_rotation + if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then + log "unable to restore the previous WARP configuration" + return 1 + fi + start_runtime +} + +rotate_once() { + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "WARP runtime exited before the scheduled identity rebuild" + return 1 + fi + + record_egress_ip + old_ip="$CURRENT_EGRESS_IP" + if [ -z "$old_ip" ]; then + log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified" + return 0 + fi + attempt=1 + if ! save_previous_config; then + log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up" + return 0 + fi + + while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do + log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}" + teardown_for_rotation + + if start_runtime; then + new_ip="$CURRENT_EGRESS_IP" + if [ -n "$new_ip" ] && + [ "$new_ip" != "$old_ip" ]; then + rm -f "$PREVIOUS_CONF" + log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}" + return 0 + fi + + if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then + log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild" + else + log "WARP egress IPv4 could not be verified; retrying identity rebuild" + fi + else + log "WARP runtime failed during identity rebuild attempt ${attempt}" + fi + + if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then + sleep "$ROTATE_RETRY_DELAY_SECONDS" + fi + attempt=$((attempt + 1)) + done + + if restore_previous_runtime; then + log "no different public IPv4 was observed; previous runtime restored" + rm -f "$PREVIOUS_CONF" + return 0 + fi + + log "rotation failed and no working runtime is available" + return 1 +} + +on_signal() { + log "received stop signal" + stop_runtime + exit 0 +} + +main() { + interval="$INTERVAL_MINUTES" + if ! is_non_negative_int "$interval"; then + log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0" + interval=0 + fi + + if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then + log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5" + ROTATE_MAX_ATTEMPTS=5 + fi + + if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then + log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5" + ROTATE_RETRY_DELAY_SECONDS=5 + fi + + if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then + log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned" + interval=0 + fi + + trap on_signal INT TERM + + if ! start_runtime; then + exit 1 + fi + if [ "$interval" -eq 0 ]; then + log "scheduled rotation disabled" + wait "$ENTRY_PID" + exit $? + fi + + log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)" + while true; do + sleep "$((interval * 60))" + log "trigger scheduled WARP identity rebuild" + if ! rotate_once; then + log "rotation failed" + exit 1 + fi + done +} + +main "$@" diff --git a/apps/microwarp/0.1.3-rotator/scripts/init.sh b/apps/microwarp/0.1.3-rotator/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/0.1.3-rotator/scripts/uninstall.sh b/apps/microwarp/0.1.3-rotator/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/0.1.3-rotator/scripts/upgrade.sh b/apps/microwarp/0.1.3-rotator/scripts/upgrade.sh new file mode 100644 index 000000000..9cba39157 --- /dev/null +++ b/apps/microwarp/0.1.3-rotator/scripts/upgrade.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" +TARGET_VERSION_DIR="0.1.3-rotator" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +resource_dir_for_app() { + local panel_root app_path app_key + + case "${APP_ROOT}" in + */apps/local/*) + panel_root="${APP_ROOT%%/apps/local/*}" + app_path="${APP_ROOT#"${panel_root}/apps/local/"}" + app_key="${app_path%%/*}" + [[ -n "${panel_root}" && -n "${app_key}" ]] || return 1 + printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}" + ;; + *) + return 1 + ;; + esac +} + +find_rotate_source() { + local resource_dir candidate_upgrade candidate_rotate + + if [[ -n "${ROTATE_SOURCE:-}" ]]; then + printf '%s\n' "${ROTATE_SOURCE}" + return 0 + fi + + if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then + candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh" + candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh" + if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \ + && cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then + printf '%s\n' "${candidate_rotate}" + return 0 + fi + fi + + if [[ -f "${APP_ROOT}/rotate.sh" ]] \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then + printf '%s\n' "${APP_ROOT}/rotate.sh" + return 0 + fi + + return 1 +} + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +rotate_target="${APP_ROOT}/rotate.sh" +rotate_source="$(find_rotate_source || true)" +if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then + echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2 + exit 1 +fi + +if [[ "${rotate_source}" != "${rotate_target}" ]] \ + && ! cmp -s "${rotate_source}" "${rotate_target}"; then + rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")" + # shellcheck disable=SC2317 + cleanup_rotate_tmp() { + rm -f -- "${rotate_tmp}" + } + trap cleanup_rotate_tmp EXIT + + cp -- "${rotate_source}" "${rotate_tmp}" + if [[ -e "${rotate_target}" ]]; then + chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true + else + chmod 0644 "${rotate_tmp}" + fi + mv -f -- "${rotate_tmp}" "${rotate_target}" + trap - EXIT + echo "Updated ${rotate_target} from the target version payload" +fi + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0 diff --git a/apps/microwarp/README.md b/apps/microwarp/README.md new file mode 100644 index 000000000..e3a2901f3 --- /dev/null +++ b/apps/microwarp/README.md @@ -0,0 +1,107 @@ +# MicroWARP(1Panel v2) + +基于 `ghcr.io/ccbkkb/microwarp` 的轻量级 Cloudflare WARP 代理封装,统一收敛为 `microwarp` 单 key,并提供官方原版与定时轮换变体。 + +## 产品介绍 + +MicroWARP 将 Cloudflare WARP 封装为可在 1Panel 中直接部署的 SOCKS5 代理;HTTP 变体通过 GOST 额外提供 HTTP 代理端口,rotator 变体负责定时重建 WARP 身份。 + +## 主要功能 + +- 支持 SOCKS5 代理及可选用户名、密码认证 +- 支持 SOCKS5-only、SOCKS5 + HTTP、定时轮换三类组合 +- 轮换前后通过代理检查真实出口 IPv4,并在同 IP 或检查失败时重试 +- 重试耗尽后恢复上一次可用 WireGuard 配置 + +## 访问说明 + +- SOCKS5 端口由 `PANEL_APP_PORT_SOCKS5` 配置 +- HTTP 变体的 HTTP 代理端口由 `PANEL_APP_PORT_HTTP_PROXY` 配置 +- 代理服务不提供 Web 管理界面;请使用对应端口和 SOCKS5/HTTP 客户端连接 +- 默认不启用认证,公网部署时建议同时设置 `SOCKS_USER` 与 `SOCKS_PASS` + +## 版本说明 + +### latest +- 官方原版 +- 仅提供 SOCKS5 代理 +- 不做自动 IP 轮换,适合需要稳定出口身份的场景 + +### http-latest +- 官方原版增强变体 +- 在 SOCKS5 基础上额外提供 HTTP 代理端口 +- 通过 `gogost/gost` sidecar 将 HTTP 转发到 SOCKS5 + +### 0.1.3-rotator +- 基于官方原版增加定时 WARP 身份重建 +- 固定使用与当前 `latest` 清单一致的官方 MicroWARP `0.1.3` 镜像 +- 轮换通过删除旧 `wg0.conf` 与账号缓存后重新执行上游初始化完成 +- 每次轮换都会通过 SOCKS5 代理读取出口 IPv4;出口未变化或无法确认时自动重试 +- 达到尝试次数仍没有新出口时恢复上一次可用配置,避免留下不可用代理 +- 轮换时会有秒级短暂中断 + +### 0.1.3-rotator-http +- 在 `0.1.3-rotator` 基础上额外提供 HTTP 代理端口 + +## 安装建议 + +- 只需官方 SOCKS5:安装 `latest` +- 需官方 SOCKS5 + HTTP:安装 `http-latest` +- 需要定时切换 WARP 身份:安装 `0.1.3-rotator` +- 需要定时切换并同时提供 HTTP:安装 `0.1.3-rotator-http` + +## 核心参数 + +- `PANEL_APP_PORT_SOCKS5`:SOCKS5 对外端口 +- `PANEL_APP_PORT_HTTP_PROXY`:HTTP 代理端口,仅 HTTP 变体使用 +- `APP_DATA_DIR_1`:WireGuard 持久化目录,保存 `wg0.conf` 与 `wgcf` 注册信息 +- `SOCKS_USER` / `SOCKS_PASS`:启用 SOCKS5 认证 +- `ENDPOINT_IP`:手动覆盖 WARP Endpoint +- `GH_PROXY`:辅助 `wgcf` 下载 +- `TAILSCALE_CIDR`:Tailscale 回程路由 CIDR,默认 `100.64.0.0/10` +- `MTU`:WireGuard MTU,默认 `1280` +- `WARP_WGCF_CONF`:可选完整 WireGuard 配置内容;也可直接将 `wg0.conf` 放入持久化目录 +- `ROTATE_INTERVAL_MINUTES`:轮换间隔(分钟),仅 `rotator-*` 版本使用,`0` 表示关闭 +- `ROTATE_MAX_ATTEMPTS`:单次轮换最多重建身份的次数,默认 `5` +- `ROTATE_RETRY_DELAY_SECONDS`:轮换重试间隔(秒),默认 `5` +- `TEST_URL`:启动时通过 SOCKS5 探测代理的 URL +- `IP_CHECK_URL`:读取出口 IPv4 的 URL,响应需要包含 `ip=`,默认使用 Cloudflare trace + +## 使用说明 + +- 默认使用外部网络 `1panel-network` +- 首次启动会自动注册 WARP 设备并生成 `/etc/wireguard/wg0.conf` +- 建议保留 `APP_DATA_DIR_1` 持久化目录,避免每次重启都重新注册 +- 官方镜像内部支持通过 GitHub token 缓解 `wgcf` 版本查询的 API 限流,但本应用默认不在 1Panel 表单中暴露该参数,优先保持默认安装简洁稳定 +- 如果设置了 `WARP_WGCF_CONF`,`rotator-*` 版本会自动关闭定时轮换,因为该配置已固定出口身份 +- `rotator-*` 版本的轮换逻辑是完整重建 WARP 身份,而不是调用上游并不存在的 rotate API;会比较轮换前后的真实出口 IPv4 +- 配置了 `SOCKS_USER` 和 `SOCKS_PASS` 时,轮换检查会使用相同的 SOCKS5 认证 + +## 升级说明 + +- 已有 `rotator-latest` 与 `rotator-http-latest` 安装可分别直接升级到 `0.1.3-rotator` 与 `0.1.3-rotator-http`,不需要中间版本;新版本名用于确保 1Panel 真正提供升级入口 +- 旧轮换版本仍保留以兼容既有安装;新安装请选择带 `0.1.3` 前缀的版本 +- 升级会保留 `APP_DATA_DIR_1` 中的 `wg0.conf`、WARP 注册信息和用户自定义环境变量;升级前仍建议使用 1Panel 备份该目录 +- 1Panel 升级脚本会将当前版本的 `rotate.sh` 原子替换到已安装目录;若安装目录仍是旧脚本且无法定位新版载荷,升级会失败而不会继续运行旧轮换逻辑,重复执行则保持幂等 +- 新增的 `ROTATE_MAX_ATTEMPTS`、`ROTATE_RETRY_DELAY_SECONDS` 与 `IP_CHECK_URL` 在旧 `.env` 缺失时使用安全默认值,不会覆盖旧的轮换间隔、端口或认证配置 +- 升级后首次轮换可能产生秒级中断;若出口 IP 无法变化或无法确认,程序会恢复上一份可用配置 + +## Introduction + +MicroWARP packages Cloudflare WARP as a 1Panel-ready SOCKS5 proxy. The HTTP variants add an HTTP proxy through GOST, while the rotator variants rebuild the WARP identity on a schedule and verify the real egress IPv4 through the proxy. + +## Features + +- SOCKS5 proxy with optional username/password authentication +- SOCKS5-only, SOCKS5 + HTTP, and scheduled-rotation variants +- Egress IPv4 verification before and after each rotation +- Retries for unchanged or unverifiable egress, with rollback to the last working WireGuard configuration + +## Upgrade notes + +- Existing `rotator-latest` and `rotator-http-latest` installations can upgrade directly to `0.1.3-rotator` and `0.1.3-rotator-http`; no intermediate version is required. The bumped package versions ensure that 1Panel exposes a real upgrade path. +- The legacy rotator versions remain for existing-install compatibility. New installations should use the `0.1.3`-prefixed variants. +- The upgrade preserves `APP_DATA_DIR_1`, including `wg0.conf`, WARP registration data, and existing user-defined environment values. Back up that directory through 1Panel before upgrading. +- The 1Panel upgrade script atomically replaces the installed `rotate.sh` with the target-version payload. If the installed script is still old and that payload cannot be located, the upgrade fails instead of continuing with the old rotation logic; reruns are idempotent. +- The new `ROTATE_MAX_ATTEMPTS`, `ROTATE_RETRY_DELAY_SECONDS`, and `IP_CHECK_URL` variables have safe Compose defaults when they are absent from an older `.env`; existing rotation interval, ports, and authentication settings are not overwritten. +- The first rotation after an upgrade may briefly interrupt the proxy. If a new or verifiable egress IP cannot be obtained, the last working configuration is restored. diff --git a/apps/microwarp/data.yml b/apps/microwarp/data.yml new file mode 100644 index 000000000..aedffdcae --- /dev/null +++ b/apps/microwarp/data.yml @@ -0,0 +1,32 @@ +name: MicroWARP +tags: + - 工具 +title: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体 +type: 工具 +description: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体 +additionalProperties: + key: microwarp + name: MicroWARP + tags: + - Tool + shortDescZh: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体 + shortDescEn: Lightweight Cloudflare WARP SOCKS5 proxy with official, HTTP, and rotator variants + description: + en: Lightweight Cloudflare WARP SOCKS5 proxy with official, HTTP, and rotator variants + zh: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体 + zh-Hant: 輕量級 Cloudflare WARP SOCKS5 代理,附帶 HTTP 與定時輪換變體 + ja: 公式版、HTTP 変種、定期ローテーション変種を備えた軽量 Cloudflare WARP SOCKS5 プロキシ + ko: 공식판, HTTP 변형, 주기적 로테이터 변형을 함께 제공하는 경량 Cloudflare WARP SOCKS5 프록시 + ru: Легковесный SOCKS5-прокси Cloudflare WARP с официальным, HTTP- и rotator-вариантами + ms: Proksi SOCKS5 Cloudflare WARP ringan dengan varian rasmi, HTTP dan rotator + pt-br: Proxy SOCKS5 leve do Cloudflare WARP com variantes oficial, HTTP e rotator + type: tool + crossVersionUpdate: true + limit: 0 + recommend: 0 + website: https://github.com/ccbkkb/MicroWARP + github: https://github.com/ccbkkb/MicroWARP + document: https://github.com/ccbkkb/MicroWARP#readme + architectures: + - amd64 + - arm64 diff --git a/apps/microwarp/http-latest/.env.sample b/apps/microwarp/http-latest/.env.sample new file mode 100644 index 000000000..643027cf2 --- /dev/null +++ b/apps/microwarp/http-latest/.env.sample @@ -0,0 +1,13 @@ +CONTAINER_NAME=microwarp +PANEL_APP_PORT_SOCKS5=1080 +PANEL_APP_PORT_HTTP_PROXY=8080 +BIND_PORT=1080 +SOCKS_USER= +SOCKS_PASS= +ENDPOINT_IP= +GH_PROXY= +TAILSCALE_CIDR=100.64.0.0/10 +MTU=1280 +WARP_WGCF_CONF= +TZ=Asia/Shanghai +APP_DATA_DIR_1=./data diff --git a/apps/microwarp/http-latest/data.yml b/apps/microwarp/http-latest/data.yml new file mode 100644 index 000000000..7f5c2b27e --- /dev/null +++ b/apps/microwarp/http-latest/data.yml @@ -0,0 +1,197 @@ +additionalProperties: + formFields: + - default: 1080 + envKey: PANEL_APP_PORT_SOCKS5 + labelEn: SOCKS5 Port + labelZh: SOCKS5 端口 + label: + en: SOCKS5 Port + zh: SOCKS5 端口 + zh-Hant: SOCKS5 連接埠 + ja: SOCKS5 ポート + ko: SOCKS5 포트 + ru: Порт SOCKS5 + ms: Port SOCKS5 + pt-br: Porta SOCKS5 + required: true + rule: paramPort + type: number + edit: true + - default: 8080 + envKey: PANEL_APP_PORT_HTTP_PROXY + labelEn: HTTP Proxy Port + labelZh: HTTP 代理端口 + label: + en: HTTP Proxy Port + zh: HTTP 代理端口 + zh-Hant: HTTP 代理連接埠 + ja: HTTP プロキシポート + ko: HTTP 프록시 포트 + ru: Порт HTTP-прокси + ms: Port proksi HTTP + pt-br: Porta do proxy HTTP + required: true + rule: paramPort + type: number + edit: true + - default: 1080 + envKey: BIND_PORT + labelEn: Internal Bind Port + labelZh: 容器内部绑定端口 + label: + en: Internal Bind Port + zh: 容器内部绑定端口 + zh-Hant: 容器內部綁定連接埠 + ja: コンテナ内部バインドポート + ko: 컨테이너 내부 바인드 포트 + ru: Внутренний порт привязки контейнера + ms: Port bind dalaman kontena + pt-br: Porta interna de bind do contêiner + required: true + rule: paramPort + type: number + edit: true + - default: "" + envKey: SOCKS_USER + labelEn: SOCKS Username + labelZh: SOCKS 用户名 + label: + en: SOCKS Username + zh: SOCKS 用户名 + zh-Hant: SOCKS 使用者名稱 + ja: SOCKS ユーザー名 + ko: SOCKS 사용자명 + ru: Имя пользователя SOCKS + ms: Nama pengguna SOCKS + pt-br: Usuário SOCKS + required: false + type: text + edit: true + - default: "" + envKey: SOCKS_PASS + labelEn: SOCKS Password + labelZh: SOCKS 密码 + label: + en: SOCKS Password + zh: SOCKS 密码 + zh-Hant: SOCKS 密碼 + ja: SOCKS パスワード + ko: SOCKS 비밀번호 + ru: Пароль SOCKS + ms: Kata laluan SOCKS + pt-br: Senha SOCKS + required: false + type: text + edit: true + - default: "" + envKey: ENDPOINT_IP + labelEn: WARP Endpoint + labelZh: WARP 终结点 + label: + en: WARP Endpoint + zh: WARP 终结点 + zh-Hant: WARP 終結點 + ja: WARP エンドポイント + ko: WARP 엔드포인트 + ru: WARP endpoint + ms: Titik akhir WARP + pt-br: Endpoint WARP + required: false + type: text + edit: true + - default: "" + envKey: GH_PROXY + labelEn: wgcf Download Proxy + labelZh: wgcf 下载代理 + label: + en: wgcf Download Proxy + zh: wgcf 下载代理 + zh-Hant: wgcf 下載代理 + ja: wgcf ダウンロードプロキシ + ko: wgcf 다운로드 프록시 + ru: Прокси для загрузки wgcf + ms: Proksi muat turun wgcf + pt-br: Proxy de download do wgcf + required: false + type: text + edit: true + - default: 100.64.0.0/10 + envKey: TAILSCALE_CIDR + labelEn: Tailscale Return Route CIDR + labelZh: Tailscale 回程路由 CIDR + label: + en: Tailscale Return Route CIDR + zh: Tailscale 回程路由 CIDR + zh-Hant: Tailscale 回程路由 CIDR + ja: Tailscale 戻りルート CIDR + ko: Tailscale 리턴 라우트 CIDR + ru: CIDR обратного маршрута Tailscale + ms: CIDR laluan balik Tailscale + pt-br: CIDR de rota de retorno do Tailscale + required: true + type: text + edit: true + - default: 1280 + envKey: MTU + labelEn: WireGuard MTU + labelZh: WireGuard MTU + label: + en: WireGuard MTU + zh: WireGuard MTU + zh-Hant: WireGuard MTU + ja: WireGuard MTU + ko: WireGuard MTU + ru: MTU WireGuard + ms: MTU WireGuard + pt-br: MTU do WireGuard + required: true + type: number + edit: true + - default: "" + envKey: WARP_WGCF_CONF + labelEn: WireGuard Config Content + labelZh: WireGuard 配置内容 + label: + en: WireGuard Config Content + zh: WireGuard 配置内容 + zh-Hant: WireGuard 設定內容 + ja: WireGuard 設定内容 + ko: WireGuard 구성 내용 + ru: Содержимое конфигурации WireGuard + ms: Kandungan konfigurasi WireGuard + pt-br: Conteúdo da configuração WireGuard + required: false + type: text + edit: true + - default: Asia/Shanghai + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text + edit: true + - default: ./data + envKey: APP_DATA_DIR_1 + labelEn: WireGuard Data Directory + labelZh: WireGuard 数据目录 + label: + en: WireGuard Data Directory + zh: WireGuard 数据目录 + zh-Hant: WireGuard 資料目錄 + ja: WireGuard データディレクトリ + ko: WireGuard 데이터 디렉터리 + ru: Каталог данных WireGuard + ms: Direktori data WireGuard + pt-br: Diretório de dados do WireGuard + required: true + type: text + edit: true diff --git a/apps/microwarp/http-latest/docker-compose.yml b/apps/microwarp/http-latest/docker-compose.yml new file mode 100644 index 000000000..c54659a9a --- /dev/null +++ b/apps/microwarp/http-latest/docker-compose.yml @@ -0,0 +1,51 @@ +services: + microwarp: + image: "ghcr.io/ccbkkb/microwarp:latest" + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + ports: + - "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}" + environment: + - TZ=${TZ} + - BIND_ADDR=0.0.0.0 + - BIND_PORT=${BIND_PORT} + - SOCKS_USER=${SOCKS_USER:-} + - SOCKS_PASS=${SOCKS_PASS:-} + - ENDPOINT_IP=${ENDPOINT_IP:-} + - GH_PROXY=${GH_PROXY:-} + - TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10} + - MTU=${MTU} + - WARP_WGCF_CONF=${WARP_WGCF_CONF:-} + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + volumes: + - "${APP_DATA_DIR_1}:/etc/wireguard" + restart: always + networks: + - 1panel-network + + http-proxy: + image: "gogost/gost:latest" + container_name: ${CONTAINER_NAME}-http + labels: + createdBy: "Apps" + depends_on: + - microwarp + command: + - "-L" + - "http://0.0.0.0:8080" + - "-F" + - "socks5://microwarp:${BIND_PORT}" + ports: + - "${PANEL_APP_PORT_HTTP_PROXY}:8080" + restart: always + networks: + - 1panel-network + +networks: + 1panel-network: + external: true diff --git a/apps/microwarp/http-latest/scripts/init.sh b/apps/microwarp/http-latest/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/http-latest/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/http-latest/scripts/uninstall.sh b/apps/microwarp/http-latest/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/http-latest/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/http-latest/scripts/upgrade.sh b/apps/microwarp/http-latest/scripts/upgrade.sh new file mode 100644 index 000000000..16806ec45 --- /dev/null +++ b/apps/microwarp/http-latest/scripts/upgrade.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0 diff --git a/apps/microwarp/latest/.env.sample b/apps/microwarp/latest/.env.sample new file mode 100644 index 000000000..873f6df15 --- /dev/null +++ b/apps/microwarp/latest/.env.sample @@ -0,0 +1,12 @@ +CONTAINER_NAME=microwarp +PANEL_APP_PORT_SOCKS5=1080 +BIND_PORT=1080 +SOCKS_USER= +SOCKS_PASS= +ENDPOINT_IP= +GH_PROXY= +TAILSCALE_CIDR=100.64.0.0/10 +MTU=1280 +WARP_WGCF_CONF= +TZ=Asia/Shanghai +APP_DATA_DIR_1=./data diff --git a/apps/microwarp/latest/data.yml b/apps/microwarp/latest/data.yml new file mode 100644 index 000000000..7cf4cd85d --- /dev/null +++ b/apps/microwarp/latest/data.yml @@ -0,0 +1,180 @@ +additionalProperties: + formFields: + - default: 1080 + envKey: PANEL_APP_PORT_SOCKS5 + labelEn: SOCKS5 Port + labelZh: SOCKS5 端口 + label: + en: SOCKS5 Port + zh: SOCKS5 端口 + zh-Hant: SOCKS5 連接埠 + ja: SOCKS5 ポート + ko: SOCKS5 포트 + ru: Порт SOCKS5 + ms: Port SOCKS5 + pt-br: Porta SOCKS5 + required: true + rule: paramPort + type: number + edit: true + - default: 1080 + envKey: BIND_PORT + labelEn: Internal Bind Port + labelZh: 容器内部绑定端口 + label: + en: Internal Bind Port + zh: 容器内部绑定端口 + zh-Hant: 容器內部綁定連接埠 + ja: コンテナ内部バインドポート + ko: 컨테이너 내부 바인드 포트 + ru: Внутренний порт привязки контейнера + ms: Port bind dalaman kontena + pt-br: Porta interna de bind do contêiner + required: true + rule: paramPort + type: number + edit: true + - default: "" + envKey: SOCKS_USER + labelEn: SOCKS Username + labelZh: SOCKS 用户名 + label: + en: SOCKS Username + zh: SOCKS 用户名 + zh-Hant: SOCKS 使用者名稱 + ja: SOCKS ユーザー名 + ko: SOCKS 사용자명 + ru: Имя пользователя SOCKS + ms: Nama pengguna SOCKS + pt-br: Usuário SOCKS + required: false + type: text + edit: true + - default: "" + envKey: SOCKS_PASS + labelEn: SOCKS Password + labelZh: SOCKS 密码 + label: + en: SOCKS Password + zh: SOCKS 密码 + zh-Hant: SOCKS 密碼 + ja: SOCKS パスワード + ko: SOCKS 비밀번호 + ru: Пароль SOCKS + ms: Kata laluan SOCKS + pt-br: Senha SOCKS + required: false + type: text + edit: true + - default: "" + envKey: ENDPOINT_IP + labelEn: WARP Endpoint + labelZh: WARP 终结点 + label: + en: WARP Endpoint + zh: WARP 终结点 + zh-Hant: WARP 終結點 + ja: WARP エンドポイント + ko: WARP 엔드포인트 + ru: WARP endpoint + ms: Titik akhir WARP + pt-br: Endpoint WARP + required: false + type: text + edit: true + - default: "" + envKey: GH_PROXY + labelEn: wgcf Download Proxy + labelZh: wgcf 下载代理 + label: + en: wgcf Download Proxy + zh: wgcf 下载代理 + zh-Hant: wgcf 下載代理 + ja: wgcf ダウンロードプロキシ + ko: wgcf 다운로드 프록시 + ru: Прокси для загрузки wgcf + ms: Proksi muat turun wgcf + pt-br: Proxy de download do wgcf + required: false + type: text + edit: true + - default: 100.64.0.0/10 + envKey: TAILSCALE_CIDR + labelEn: Tailscale Return Route CIDR + labelZh: Tailscale 回程路由 CIDR + label: + en: Tailscale Return Route CIDR + zh: Tailscale 回程路由 CIDR + zh-Hant: Tailscale 回程路由 CIDR + ja: Tailscale 戻りルート CIDR + ko: Tailscale 리턴 라우트 CIDR + ru: CIDR обратного маршрута Tailscale + ms: CIDR laluan balik Tailscale + pt-br: CIDR de rota de retorno do Tailscale + required: true + type: text + edit: true + - default: 1280 + envKey: MTU + labelEn: WireGuard MTU + labelZh: WireGuard MTU + label: + en: WireGuard MTU + zh: WireGuard MTU + zh-Hant: WireGuard MTU + ja: WireGuard MTU + ko: WireGuard MTU + ru: MTU WireGuard + ms: MTU WireGuard + pt-br: MTU do WireGuard + required: true + type: number + edit: true + - default: "" + envKey: WARP_WGCF_CONF + labelEn: WireGuard Config Content + labelZh: WireGuard 配置内容 + label: + en: WireGuard Config Content + zh: WireGuard 配置内容 + zh-Hant: WireGuard 設定內容 + ja: WireGuard 設定内容 + ko: WireGuard 구성 내용 + ru: Содержимое конфигурации WireGuard + ms: Kandungan konfigurasi WireGuard + pt-br: Conteúdo da configuração WireGuard + required: false + type: text + edit: true + - default: Asia/Shanghai + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text + edit: true + - default: ./data + envKey: APP_DATA_DIR_1 + labelEn: WireGuard Data Directory + labelZh: WireGuard 数据目录 + label: + en: WireGuard Data Directory + zh: WireGuard 数据目录 + zh-Hant: WireGuard 資料目錄 + ja: WireGuard データディレクトリ + ko: WireGuard 데이터 디렉터리 + ru: Каталог данных WireGuard + ms: Direktori data WireGuard + pt-br: Diretório de dados do WireGuard + required: true + type: text + edit: true diff --git a/apps/microwarp/latest/docker-compose.yml b/apps/microwarp/latest/docker-compose.yml new file mode 100644 index 000000000..2f769a18d --- /dev/null +++ b/apps/microwarp/latest/docker-compose.yml @@ -0,0 +1,33 @@ +services: + microwarp: + image: "ghcr.io/ccbkkb/microwarp:latest" + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + ports: + - "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}" + environment: + - TZ=${TZ} + - BIND_ADDR=0.0.0.0 + - BIND_PORT=${BIND_PORT} + - SOCKS_USER=${SOCKS_USER:-} + - SOCKS_PASS=${SOCKS_PASS:-} + - ENDPOINT_IP=${ENDPOINT_IP:-} + - GH_PROXY=${GH_PROXY:-} + - TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10} + - MTU=${MTU} + - WARP_WGCF_CONF=${WARP_WGCF_CONF:-} + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + volumes: + - "${APP_DATA_DIR_1}:/etc/wireguard" + restart: always + networks: + - 1panel-network + +networks: + 1panel-network: + external: true diff --git a/apps/microwarp/latest/scripts/init.sh b/apps/microwarp/latest/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/latest/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/latest/scripts/uninstall.sh b/apps/microwarp/latest/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/latest/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/latest/scripts/upgrade.sh b/apps/microwarp/latest/scripts/upgrade.sh new file mode 100644 index 000000000..16806ec45 --- /dev/null +++ b/apps/microwarp/latest/scripts/upgrade.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0 diff --git a/apps/microwarp/logo.png b/apps/microwarp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9cec3e62d1817bf3fe702d0d21ccdd15beb049a2 GIT binary patch literal 4768 zcmV;R5?}3!P)jbaz0;>2SrtTwn z)H1E<6|nIfMA{#)@DVcPBd_i=ZPq7b*aVQ=DY5Pzci0(h))HveB68OhgW4TK;2AyS zBR=37iQ5q&-xx{y0kHG{i`f8%*aIc!0CLy^Eam`g*8zFh0f5*89p?dM*a14?0yE?Q zUfBUw+5te|0Z!ZiklO)A-Ud4LYx6xd0000XbW%=J06iXX(G4#H{Qi$>R_ar^=A=Y? z`hl*|>X+iP&Xvja=Uoc^!vFvgdPzhopiyCwmZ88QH4lWH^WzlzO}Do-^o zG86ad{=WP0SY^N~LR^j|cbF5!i)YpDKR~n|<@)eG9$JHHC+u z!M(djZt_vB>;D~WjK3;H*7n7F0(&%S-TxJ9>-Z}UIQp6XNJQ%RJ1-r7g6vVfkXn=--}4@RcYp{Upn^i)nATC8hV}rzHarg_a`X!ksrx_IxPlM+F#3gvG&5M>cwqMA;7NGo$09Nxf1&#u zU__37Dk4K8{z@KE^^rdlk@(%p!Q2=I?ERTIBJDYEaNFMRU@fW&ahekhyO0>SIkHl}1BiAzV2ZyQH9xeZ5_zYc% zeandC@mDK~KRA5!n!dKJu;H`6yl0*)dRHEkw~9y{3XAb)Haj83CQZ}n%k_>C-e~v% z&3tkwBJy^9BzIlyT>SaN4K~I-yetyDE`I;`m?HKtXR+J$ku?5ltsHU~cQ&%6>HF}4 zNbK?UD?^?FHDiB|3nTId^GF?kF#gPm(`LwPXcD{AICC_#5#z-s#O~y*eTzPl51B=K zsASC?y{^~$^_oy`rqjZRL>n>IbSfL?evJwv@(cYfpNh(q5oD1oFARCV-V1JNl4+WY zNHlY2I%Ux+vAIj$vk|H1k>&bGI%VZwRwn)!EpL)~t-L*~!QFC)khZIJnTG_ElBxJ~3%Z4!gZFpeF?0S;e`<&XK9Bt7n zVyFC)rW@4yBPC*=k4TF|VszR0lo7@UY$rucxJ4%EFpttN6l35&z((Ya^fgV{@41Nd zL`06R4z@3*49Eh5gKivqZ^+HC3p#VmHbQKG-b}gHDgp9#E+R)FkrZ27r;J)r%82^A z!*0i}5?P6@vCj;fLc`iRiI~nVib!+%dwG>BGiJeq3S>cjAP$k_nRv>)yRx?6)>2O| zL}Ykrfrb70a6V;JlHGPEo2E9beRklX?FH6GJC+{g~0N zwp#<7aI@=S&~Te(sLm!OLy>7-?QBF!H#Fs=@T;jjhD~Fzg0@|)B(pF#;f~$sp`x?N zV}H=3UC>7&X3N(?k7_}NStGBA+NMjAax;FML@;bUVpU>O>~~{C!0z2aFm}ZenWIM! zUTDVOYBgW21UK~vQp1nkOYoIdQ|N5cG&QkJXY+_8FM0w;@D;59(qM%q6F-@+=InBg z8^8zcFU^@nXG2Ufofbsoi@|jVyGZWXozQF6YokZ{w_q@z8_ap^K^%T@;SDwc#Q)`} z4O@dpjDaqAqKnYoI=?5Gqq%H~d&`ZD*l?5FV);sRHp(T1S%d{7^@q?m^d9OYlK}H< zPO(>1Zn)X@f*UOqiq0k#*g6(7)H*ja)GFSi&@8bGW~@nMCH7WYWYv#uwoN1W~qG&9FcG~y7#oZS920}lU`vlTWC z$yZM+U02X`1>Ku1xYRQ<91*>tytW9k#q-K|n zFQMxNN&~c@p7RT~Qf3ZIbvAS|BdmQDdcmQ!*sKkrpW7=Cb*7XWJpK7$1HF*ZIOpV1 zxzqMRDeN}w&v_(TDZL=RW^ECaT9l7gl`@uU(_rM}pSDVC>_sVP&+D&}ve2g1AJ^@% zTo8xwo1oOeS!Yx!5sYnP5P~{?+CodM7iE?1L#JO3IS=c%~v+H0rg(mP^X+2d=_OlUQvO-ewM(~iLXX#>5? za4!Sb1IJ_-?j|)?nre+J@0Rvp%mRDqth6uIO>i8ZxIz=H}r#0^?d zlYDKVv|*5?8?+D_TH^I)&Qf!w*905EuPZ$TKM>5%p%=)>eBuU8x9M!`>@gwO z3rA=obmaM7BB<@9=FoyV-NMIdIyc2JrL%}3!qJc}k5GJB5DN^9W|3#ARQ*-rN>AME;Sc!Ca?wzyN&0d%&;i=qRk_)GT0#@Ct)Yd`jQd%iDr?Kt zSGzZ1FHn0bHTN~>$(S;O-1bU?(AXj+bm+aIJ!Y0tbLhQkkj=&tT4PTpwjwoV0=iF{ zI$37*E(pEX`&ZYt&|{O@-W~%zt(?&YN?S@zl-6p0UWZmvyD5!AFZ1h{l6z()wNQG$ zHdBa&J;sC@nx#GZdSaf@zmS#*^NUflaXk6PsBl6e< z+VlE+mLnE0-3@QG>|_tGuU1QzNgz)m;z_Znr?LB1(43k|Z3Rua$Je1zXx^UVY#AJzcJ-7=u1O8h zC#Ce>4ch2qER^mG}@z};5-Wx;i1?SI)V@x(7Ci@mjpNu*K_~)SX*iPxtYv!z6 ze0u+`N9G5Ypiic+EtEc*=CDvYiG7RBAegz6o|V$Hgl2zL>9L{o*a{lIe5)}PnA^hB zT4|y7Rp_Gz78je&#bI}Z6|~fv*W%ejb5DtMB`6G>vFc!IeUaT=wl{(G=J>B#9@aSp0E_f;68Q2t4aej1^?Il zp^aCt6Y`K_J{3Vf(S3Pn4^wM)x!LC9E7`C?%J4MysSuhdeMF^?V-tHkNq%56Vf1=X zC00hohwZVP@&zp)Dot8qm6s zAvnrr_?kO}S$lcY^oRA1W>FX^z&r6DiGtJB7Ze)40`2OfnXTor!p+hjmW=3Xwp>N5 zD0kFPf1rg7by}SBAgFV;R@}Eqe=s9RRGm_`+>y5(GAluKaSphfmO+IN$0FjuM zJ^hJFSY2L43u5^dosy6Pg|aj*^zImR&DgYaf6OPo%(~OE_vQblE9lF*!Btjk(aW$) z!y_Jmf-_M^6t)<(S=tx89gC9)T?THHPk-o)e7U1pTFQ|7fr0g?swhPfWyLH=Z2Z&M zMM?wpb4ibcyhd|NYuRLFDWx7X%yPsg6kK;@-wl=fRr3j+&zvo>Gjkce;9j=C0ctdh zs|I&E`MffJ^nwOuXT zH%s`s5IC!s)k8}OC?#$@LwAdk>T|y zT%l*ANc0R-u=f@AIg(!QPXTf;&F=4{MIZkT;7u^I7mZkLG`l^Ua;I}UFh#;MUo@HZ z@S4@e1!oOc7NLp`O(SL>FQ6tEp*@+q%tyUH|^d<9*r?r2wNffYi&}1&)zq<}R-se=Kr>in zr06QWT$p6cJoPosQgPlcAv={967SdKRu?ZXlCBNMsb62LIny}z1@+H1JQA|G<49>9 za0*)E7+H)>5(NY`zmA)n#0@^>He^B&T)C<|QS= zfuj2m;&w&%3AC3Jl9P07>(^&gZQ>cE$MK6Vwl3?Gl2M1$*Fj87r;HpVWQ7Fd(XDk$ zKiakbiTQz7QQ%_3KdibDy9_27!|9_A5iOaZAS5!QV{+8VrupDThih$&wQJrb8F?^B zDTBSHB4B6883c7qr;u%L%; zN#vWF(Ln7Psl9v~`l}#qmEvUd9#6kxJot93d70XWJE6prr{2V|p30`EomF1v9}Aay z!qE(&kkn(u%M|gKm)fsO0mfuC>9~>52XC4XM^4$B<@EQeY=eI7TjsXRFZY-;lEcwW z21gM;(oJgi(x2MPD~8axE%Uy*+w2@R*s01L_cKBM%NCN1gWO%?FP_>RM|W5-U3p|3 zJt7?3Re#KVV$%?OaT^U2ly@?TbJuRF=sw+!lfSVR(Men9;7r1scDERsx~X3VM_ zS`i-qNHSSG(PUVY+RgEIX{

!}0k@$uIak@e=yjlY=N3BnWaKW~gk*dW)%cD3KH zwneSf(5a7)G8=QG9&G%o^pIm^L|i@bbY{D?PdIy50b`l(y!p* u)zeQLhv-{v{-Yu3Xm({aFaH6 255) exit 1 + } + exit 0 + } + { exit 1 } + ' +} + +probe_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + curl_via_socks "$TEST_URL" >/dev/null 2>&1 +} + +curl_via_socks() { + url="$1" + if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \ + --proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url" + return $? + fi + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url" +} + +get_egress_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + + trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1 + current_ip="$(printf '%s\n' "$trace_output" | + awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')" + is_ipv4 "$current_ip" && printf '%s\n' "$current_ip" +} + +record_egress_ip() { + current_ip="$(get_egress_ip || true)" + if [ -n "$current_ip" ]; then + CURRENT_EGRESS_IP="$current_ip" + log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}" + else + CURRENT_EGRESS_IP="" + log "unable to verify current WARP egress IPv4" + fi + return 0 +} + +inject_fixed_config() { + if [ -z "$FIXED_CONFIG" ]; then + return 0 + fi + mkdir -p "$WG_DIR" + printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF" + chmod 600 "$WG_CONF" || true + log "wg0.conf injected from WARP_WGCF_CONF" +} + +wait_for_proxy() { + attempts=0 + while [ "$attempts" -lt 30 ]; do + if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then + wait "$ENTRY_PID" || true + ENTRY_PID="" + return 1 + fi + + if probe_ip; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 2 + done + return 1 +} + +stop_runtime() { + if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then + kill "$ENTRY_PID" 2>/dev/null || true + wait "$ENTRY_PID" || true + fi + ENTRY_PID="" +} + +teardown_for_rotation() { + stop_runtime + if ip link show wg0 >/dev/null 2>&1; then + wg-quick down wg0 >/dev/null 2>&1 || true + fi + rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env" +} + +start_runtime() { + CURRENT_EGRESS_IP="" + inject_fixed_config + log "starting MicroWARP runtime" + /app/entrypoint.sh & + ENTRY_PID=$! + if ! wait_for_proxy; then + log "runtime did not become ready in time" + stop_runtime + return 1 + fi + record_egress_ip + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "runtime exited while its egress was being checked" + stop_runtime + return 1 + fi + return 0 +} + +save_previous_config() { + if [ -s "$WG_CONF" ]; then + previous_tmp="${PREVIOUS_CONF}.tmp.$$" + rm -f "$previous_tmp" + if ! cp -p "$WG_CONF" "$previous_tmp"; then + rm -f "$previous_tmp" + return 1 + fi + if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then + rm -f "$previous_tmp" + return 1 + fi + return 0 + fi + return 1 +} + +restore_previous_runtime() { + if [ ! -s "$PREVIOUS_CONF" ]; then + return 1 + fi + + log "restoring the previous working WARP configuration" + teardown_for_rotation + if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then + log "unable to restore the previous WARP configuration" + return 1 + fi + start_runtime +} + +rotate_once() { + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "WARP runtime exited before the scheduled identity rebuild" + return 1 + fi + + record_egress_ip + old_ip="$CURRENT_EGRESS_IP" + if [ -z "$old_ip" ]; then + log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified" + return 0 + fi + attempt=1 + if ! save_previous_config; then + log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up" + return 0 + fi + + while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do + log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}" + teardown_for_rotation + + if start_runtime; then + new_ip="$CURRENT_EGRESS_IP" + if [ -n "$new_ip" ] && + [ "$new_ip" != "$old_ip" ]; then + rm -f "$PREVIOUS_CONF" + log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}" + return 0 + fi + + if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then + log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild" + else + log "WARP egress IPv4 could not be verified; retrying identity rebuild" + fi + else + log "WARP runtime failed during identity rebuild attempt ${attempt}" + fi + + if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then + sleep "$ROTATE_RETRY_DELAY_SECONDS" + fi + attempt=$((attempt + 1)) + done + + if restore_previous_runtime; then + log "no different public IPv4 was observed; previous runtime restored" + rm -f "$PREVIOUS_CONF" + return 0 + fi + + log "rotation failed and no working runtime is available" + return 1 +} + +on_signal() { + log "received stop signal" + stop_runtime + exit 0 +} + +main() { + interval="$INTERVAL_MINUTES" + if ! is_non_negative_int "$interval"; then + log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0" + interval=0 + fi + + if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then + log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5" + ROTATE_MAX_ATTEMPTS=5 + fi + + if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then + log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5" + ROTATE_RETRY_DELAY_SECONDS=5 + fi + + if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then + log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned" + interval=0 + fi + + trap on_signal INT TERM + + if ! start_runtime; then + exit 1 + fi + if [ "$interval" -eq 0 ]; then + log "scheduled rotation disabled" + wait "$ENTRY_PID" + exit $? + fi + + log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)" + while true; do + sleep "$((interval * 60))" + log "trigger scheduled WARP identity rebuild" + if ! rotate_once; then + log "rotation failed" + exit 1 + fi + done +} + +main "$@" diff --git a/apps/microwarp/rotator-http-latest/scripts/init.sh b/apps/microwarp/rotator-http-latest/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/rotator-http-latest/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/rotator-http-latest/scripts/uninstall.sh b/apps/microwarp/rotator-http-latest/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/rotator-http-latest/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/rotator-http-latest/scripts/upgrade.sh b/apps/microwarp/rotator-http-latest/scripts/upgrade.sh new file mode 100644 index 000000000..fe1e090e4 --- /dev/null +++ b/apps/microwarp/rotator-http-latest/scripts/upgrade.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" +TARGET_VERSION_DIR="0.1.3-rotator-http" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +resource_dir_for_app() { + local panel_root app_path app_key + + case "${APP_ROOT}" in + */apps/local/*) + panel_root="${APP_ROOT%%/apps/local/*}" + app_path="${APP_ROOT#"${panel_root}/apps/local/"}" + app_key="${app_path%%/*}" + [[ -n "${panel_root}" && -n "${app_key}" ]] || return 1 + printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}" + ;; + *) + return 1 + ;; + esac +} + +find_rotate_source() { + local resource_dir candidate_upgrade candidate_rotate + + if [[ -n "${ROTATE_SOURCE:-}" ]]; then + printf '%s\n' "${ROTATE_SOURCE}" + return 0 + fi + + if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then + candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh" + candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh" + if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \ + && cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then + printf '%s\n' "${candidate_rotate}" + return 0 + fi + fi + + if [[ -f "${APP_ROOT}/rotate.sh" ]] \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then + printf '%s\n' "${APP_ROOT}/rotate.sh" + return 0 + fi + + return 1 +} + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +rotate_target="${APP_ROOT}/rotate.sh" +rotate_source="$(find_rotate_source || true)" +if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then + echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2 + exit 1 +fi + +if [[ "${rotate_source}" != "${rotate_target}" ]] \ + && ! cmp -s "${rotate_source}" "${rotate_target}"; then + rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")" + # shellcheck disable=SC2317 + cleanup_rotate_tmp() { + rm -f -- "${rotate_tmp}" + } + trap cleanup_rotate_tmp EXIT + + cp -- "${rotate_source}" "${rotate_tmp}" + if [[ -e "${rotate_target}" ]]; then + chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true + else + chmod 0644 "${rotate_tmp}" + fi + mv -f -- "${rotate_tmp}" "${rotate_target}" + trap - EXIT + echo "Updated ${rotate_target} from the target version payload" +fi + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0 diff --git a/apps/microwarp/rotator-latest/.env.sample b/apps/microwarp/rotator-latest/.env.sample new file mode 100644 index 000000000..3c40a9f80 --- /dev/null +++ b/apps/microwarp/rotator-latest/.env.sample @@ -0,0 +1,17 @@ +CONTAINER_NAME=microwarp +PANEL_APP_PORT_SOCKS5=1080 +BIND_PORT=1080 +ROTATE_INTERVAL_MINUTES=0 +ROTATE_MAX_ATTEMPTS=5 +ROTATE_RETRY_DELAY_SECONDS=5 +TEST_URL=https://cloudflare.com/cdn-cgi/trace +IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace +SOCKS_USER= +SOCKS_PASS= +ENDPOINT_IP= +GH_PROXY= +TAILSCALE_CIDR=100.64.0.0/10 +MTU=1280 +WARP_WGCF_CONF= +TZ=Asia/Shanghai +APP_DATA_DIR_1=./data diff --git a/apps/microwarp/rotator-latest/data.yml b/apps/microwarp/rotator-latest/data.yml new file mode 100644 index 000000000..fa0b013f3 --- /dev/null +++ b/apps/microwarp/rotator-latest/data.yml @@ -0,0 +1,260 @@ +additionalProperties: + formFields: + - default: 1080 + envKey: PANEL_APP_PORT_SOCKS5 + labelEn: SOCKS5 Port + labelZh: SOCKS5 端口 + label: + en: SOCKS5 Port + zh: SOCKS5 端口 + zh-Hant: SOCKS5 連接埠 + ja: SOCKS5 ポート + ko: SOCKS5 포트 + ru: Порт SOCKS5 + ms: Port SOCKS5 + pt-br: Porta SOCKS5 + required: true + rule: paramPort + type: number + edit: true + - default: 1080 + envKey: BIND_PORT + labelEn: Internal Bind Port + labelZh: 容器内部绑定端口 + label: + en: Internal Bind Port + zh: 容器内部绑定端口 + zh-Hant: 容器內部綁定連接埠 + ja: コンテナ内部バインドポート + ko: 컨테이너 내부 바인드 포트 + ru: Внутренний порт привязки контейнера + ms: Port bind dalaman kontena + pt-br: Porta interna de bind do contêiner + required: true + rule: paramPort + type: number + edit: true + - default: 0 + envKey: ROTATE_INTERVAL_MINUTES + labelEn: Rotate Interval Minutes + labelZh: 轮换间隔(分钟) + label: + en: Rotate Interval Minutes + zh: 轮换间隔(分钟) + zh-Hant: 輪換間隔(分鐘) + ja: ローテーション間隔(分) + ko: 로테이션 간격(분) + ru: Интервал ротации (минуты) + ms: Selang putaran (minit) + pt-br: Intervalo de rotação (minutos) + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_MAX_ATTEMPTS + labelEn: Rotation Attempts + labelZh: 轮换尝试次数 + label: + en: Rotation Attempts + zh: 轮换尝试次数 + zh-Hant: 輪換嘗試次數 + ja: ローテーション試行回数 + ko: 로테이션 시도 횟수 + ru: Количество попыток ротации + ms: Bilangan percubaan putaran + pt-br: Número de tentativas de rotação + required: true + type: number + edit: true + - default: 5 + envKey: ROTATE_RETRY_DELAY_SECONDS + labelEn: Rotation Retry Delay Seconds + labelZh: 轮换重试间隔(秒) + label: + en: Rotation Retry Delay Seconds + zh: 轮换重试间隔(秒) + zh-Hant: 輪換重試間隔(秒) + ja: ローテーション再試行間隔(秒) + ko: 로테이션 재시도 간격(초) + ru: Задержка между попытками ротации (секунды) + ms: Kelewatan percubaan semula putaran (saat) + pt-br: Atraso entre tentativas de rotação (segundos) + required: true + type: number + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: TEST_URL + labelEn: Probe URL + labelZh: 探测 URL + label: + en: Probe URL + zh: 探测 URL + zh-Hant: 探測 URL + ja: プローブ URL + ko: 점검 URL + ru: URL проверки + ms: URL ujian + pt-br: URL de verificação + required: true + type: text + edit: true + - default: https://cloudflare.com/cdn-cgi/trace + envKey: IP_CHECK_URL + labelEn: Egress IP Check URL + labelZh: 出口 IP 检查 URL + label: + en: Egress IP Check URL + zh: 出口 IP 检查 URL + zh-Hant: 出口 IP 檢查 URL + ja: 出口 IP 確認 URL + ko: 출구 IP 확인 URL + ru: URL проверки исходного IP + ms: URL semakan IP keluar + pt-br: URL de verificação do IP de saída + required: true + type: text + edit: true + - default: "" + envKey: SOCKS_USER + labelEn: SOCKS Username + labelZh: SOCKS 用户名 + label: + en: SOCKS Username + zh: SOCKS 用户名 + zh-Hant: SOCKS 使用者名稱 + ja: SOCKS ユーザー名 + ko: SOCKS 사용자명 + ru: Имя пользователя SOCKS + ms: Nama pengguna SOCKS + pt-br: Usuário SOCKS + required: false + type: text + edit: true + - default: "" + envKey: SOCKS_PASS + labelEn: SOCKS Password + labelZh: SOCKS 密码 + label: + en: SOCKS Password + zh: SOCKS 密码 + zh-Hant: SOCKS 密碼 + ja: SOCKS パスワード + ko: SOCKS 비밀번호 + ru: Пароль SOCKS + ms: Kata laluan SOCKS + pt-br: Senha SOCKS + required: false + type: text + edit: true + - default: "" + envKey: ENDPOINT_IP + labelEn: WARP Endpoint + labelZh: WARP 终结点 + label: + en: WARP Endpoint + zh: WARP 终结点 + zh-Hant: WARP 終結點 + ja: WARP エンドポイント + ko: WARP 엔드포인트 + ru: WARP endpoint + ms: Titik akhir WARP + pt-br: Endpoint WARP + required: false + type: text + edit: true + - default: "" + envKey: GH_PROXY + labelEn: wgcf Download Proxy + labelZh: wgcf 下载代理 + label: + en: wgcf Download Proxy + zh: wgcf 下载代理 + zh-Hant: wgcf 下載代理 + ja: wgcf ダウンロードプロキシ + ko: wgcf 다운로드 프록시 + ru: Прокси для загрузки wgcf + ms: Proksi muat turun wgcf + pt-br: Proxy de download do wgcf + required: false + type: text + edit: true + - default: 100.64.0.0/10 + envKey: TAILSCALE_CIDR + labelEn: Tailscale Return Route CIDR + labelZh: Tailscale 回程路由 CIDR + label: + en: Tailscale Return Route CIDR + zh: Tailscale 回程路由 CIDR + zh-Hant: Tailscale 回程路由 CIDR + ja: Tailscale 戻りルート CIDR + ko: Tailscale 리턴 라우트 CIDR + ru: CIDR обратного маршрута Tailscale + ms: CIDR laluan balik Tailscale + pt-br: CIDR de rota de retorno do Tailscale + required: true + type: text + edit: true + - default: 1280 + envKey: MTU + labelEn: WireGuard MTU + labelZh: WireGuard MTU + label: + en: WireGuard MTU + zh: WireGuard MTU + zh-Hant: WireGuard MTU + ja: WireGuard MTU + ko: WireGuard MTU + ru: MTU WireGuard + ms: MTU WireGuard + pt-br: MTU do WireGuard + required: true + type: number + edit: true + - default: "" + envKey: WARP_WGCF_CONF + labelEn: WireGuard Config Content + labelZh: WireGuard 配置内容 + label: + en: WireGuard Config Content + zh: WireGuard 配置内容 + zh-Hant: WireGuard 設定內容 + ja: WireGuard 設定内容 + ko: WireGuard 구성 내용 + ru: Содержимое конфигурации WireGuard + ms: Kandungan konfigurasi WireGuard + pt-br: Conteúdo da configuração WireGuard + required: false + type: text + edit: true + - default: Asia/Shanghai + envKey: TZ + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horário + required: true + type: text + edit: true + - default: ./data + envKey: APP_DATA_DIR_1 + labelEn: WireGuard Data Directory + labelZh: WireGuard 数据目录 + label: + en: WireGuard Data Directory + zh: WireGuard 数据目录 + zh-Hant: WireGuard 資料目錄 + ja: WireGuard データディレクトリ + ko: WireGuard 데이터 디렉터리 + ru: Каталог данных WireGuard + ms: Direktori data WireGuard + pt-br: Diretório de dados do WireGuard + required: true + type: text + edit: true diff --git a/apps/microwarp/rotator-latest/docker-compose.yml b/apps/microwarp/rotator-latest/docker-compose.yml new file mode 100644 index 000000000..d54706150 --- /dev/null +++ b/apps/microwarp/rotator-latest/docker-compose.yml @@ -0,0 +1,40 @@ +services: + microwarp: + image: "ghcr.io/ccbkkb/microwarp:0.1.3" + container_name: ${CONTAINER_NAME} + labels: + createdBy: "Apps" + ports: + - "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}" + environment: + - TZ=${TZ} + - BIND_ADDR=0.0.0.0 + - BIND_PORT=${BIND_PORT} + - ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0} + - ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5} + - ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5} + - TEST_URL=${TEST_URL} + - IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace} + - SOCKS_USER=${SOCKS_USER:-} + - SOCKS_PASS=${SOCKS_PASS:-} + - ENDPOINT_IP=${ENDPOINT_IP:-} + - GH_PROXY=${GH_PROXY:-} + - TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10} + - MTU=${MTU} + - WARP_WGCF_CONF=${WARP_WGCF_CONF:-} + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + volumes: + - "${APP_DATA_DIR_1}:/etc/wireguard" + - "./rotate.sh:/usr/local/bin/rotate.sh:ro" + restart: always + command: ["/bin/sh", "/usr/local/bin/rotate.sh"] + networks: + - 1panel-network + +networks: + 1panel-network: + external: true diff --git a/apps/microwarp/rotator-latest/rotate.sh b/apps/microwarp/rotator-latest/rotate.sh new file mode 100644 index 000000000..c3b9dbe3d --- /dev/null +++ b/apps/microwarp/rotator-latest/rotate.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env sh +set -eu + +INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}" +ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}" +ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}" +TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}" +IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}" +SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}" +SOCKS_USER_VALUE="${SOCKS_USER:-}" +SOCKS_PASS_VALUE="${SOCKS_PASS:-}" +WG_DIR="/etc/wireguard" +WG_CONF="${WG_DIR}/wg0.conf" +PREVIOUS_CONF="${WG_CONF}.rotation-previous" +FIXED_CONFIG="${WARP_WGCF_CONF:-}" +LOG_PREFIX="[microwarp]" +ENTRY_PID="" +CURRENT_EGRESS_IP="" + +log() { + echo "${LOG_PREFIX} $*" +} + +is_non_negative_int() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_positive_int() { + case "$1" in + ''|*[!0-9]*|0) + return 1 + ;; + *) + return 0 + ;; + esac +} + +is_ipv4() { + printf '%s\n' "$1" | awk -F. ' + NF == 4 { + for (i = 1; i <= 4; i++) { + if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1 + } + exit 0 + } + { exit 1 } + ' +} + +probe_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + curl_via_socks "$TEST_URL" >/dev/null 2>&1 +} + +curl_via_socks() { + url="$1" + if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \ + --proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url" + return $? + fi + curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url" +} + +get_egress_ip() { + if ! command -v curl >/dev/null 2>&1; then + return 1 + fi + + trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1 + current_ip="$(printf '%s\n' "$trace_output" | + awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')" + is_ipv4 "$current_ip" && printf '%s\n' "$current_ip" +} + +record_egress_ip() { + current_ip="$(get_egress_ip || true)" + if [ -n "$current_ip" ]; then + CURRENT_EGRESS_IP="$current_ip" + log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}" + else + CURRENT_EGRESS_IP="" + log "unable to verify current WARP egress IPv4" + fi + return 0 +} + +inject_fixed_config() { + if [ -z "$FIXED_CONFIG" ]; then + return 0 + fi + mkdir -p "$WG_DIR" + printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF" + chmod 600 "$WG_CONF" || true + log "wg0.conf injected from WARP_WGCF_CONF" +} + +wait_for_proxy() { + attempts=0 + while [ "$attempts" -lt 30 ]; do + if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then + wait "$ENTRY_PID" || true + ENTRY_PID="" + return 1 + fi + + if probe_ip; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 2 + done + return 1 +} + +stop_runtime() { + if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then + kill "$ENTRY_PID" 2>/dev/null || true + wait "$ENTRY_PID" || true + fi + ENTRY_PID="" +} + +teardown_for_rotation() { + stop_runtime + if ip link show wg0 >/dev/null 2>&1; then + wg-quick down wg0 >/dev/null 2>&1 || true + fi + rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env" +} + +start_runtime() { + CURRENT_EGRESS_IP="" + inject_fixed_config + log "starting MicroWARP runtime" + /app/entrypoint.sh & + ENTRY_PID=$! + if ! wait_for_proxy; then + log "runtime did not become ready in time" + stop_runtime + return 1 + fi + record_egress_ip + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "runtime exited while its egress was being checked" + stop_runtime + return 1 + fi + return 0 +} + +save_previous_config() { + if [ -s "$WG_CONF" ]; then + previous_tmp="${PREVIOUS_CONF}.tmp.$$" + rm -f "$previous_tmp" + if ! cp -p "$WG_CONF" "$previous_tmp"; then + rm -f "$previous_tmp" + return 1 + fi + if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then + rm -f "$previous_tmp" + return 1 + fi + return 0 + fi + return 1 +} + +restore_previous_runtime() { + if [ ! -s "$PREVIOUS_CONF" ]; then + return 1 + fi + + log "restoring the previous working WARP configuration" + teardown_for_rotation + if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then + log "unable to restore the previous WARP configuration" + return 1 + fi + start_runtime +} + +rotate_once() { + if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then + log "WARP runtime exited before the scheduled identity rebuild" + return 1 + fi + + record_egress_ip + old_ip="$CURRENT_EGRESS_IP" + if [ -z "$old_ip" ]; then + log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified" + return 0 + fi + attempt=1 + if ! save_previous_config; then + log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up" + return 0 + fi + + while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do + log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}" + teardown_for_rotation + + if start_runtime; then + new_ip="$CURRENT_EGRESS_IP" + if [ -n "$new_ip" ] && + [ "$new_ip" != "$old_ip" ]; then + rm -f "$PREVIOUS_CONF" + log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}" + return 0 + fi + + if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then + log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild" + else + log "WARP egress IPv4 could not be verified; retrying identity rebuild" + fi + else + log "WARP runtime failed during identity rebuild attempt ${attempt}" + fi + + if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then + sleep "$ROTATE_RETRY_DELAY_SECONDS" + fi + attempt=$((attempt + 1)) + done + + if restore_previous_runtime; then + log "no different public IPv4 was observed; previous runtime restored" + rm -f "$PREVIOUS_CONF" + return 0 + fi + + log "rotation failed and no working runtime is available" + return 1 +} + +on_signal() { + log "received stop signal" + stop_runtime + exit 0 +} + +main() { + interval="$INTERVAL_MINUTES" + if ! is_non_negative_int "$interval"; then + log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0" + interval=0 + fi + + if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then + log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5" + ROTATE_MAX_ATTEMPTS=5 + fi + + if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then + log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5" + ROTATE_RETRY_DELAY_SECONDS=5 + fi + + if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then + log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned" + interval=0 + fi + + trap on_signal INT TERM + + if ! start_runtime; then + exit 1 + fi + if [ "$interval" -eq 0 ]; then + log "scheduled rotation disabled" + wait "$ENTRY_PID" + exit $? + fi + + log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)" + while true; do + sleep "$((interval * 60))" + log "trigger scheduled WARP identity rebuild" + if ! rotate_once; then + log "rotation failed" + exit 1 + fi + done +} + +main "$@" diff --git a/apps/microwarp/rotator-latest/scripts/init.sh b/apps/microwarp/rotator-latest/scripts/init.sh new file mode 100644 index 000000000..256b46b50 --- /dev/null +++ b/apps/microwarp/rotator-latest/scripts/init.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" diff --git a/apps/microwarp/rotator-latest/scripts/uninstall.sh b/apps/microwarp/rotator-latest/scripts/uninstall.sh new file mode 100755 index 000000000..ab8a8d674 --- /dev/null +++ b/apps/microwarp/rotator-latest/scripts/uninstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker-compose down --volumes diff --git a/apps/microwarp/rotator-latest/scripts/upgrade.sh b/apps/microwarp/rotator-latest/scripts/upgrade.sh new file mode 100644 index 000000000..9cba39157 --- /dev/null +++ b/apps/microwarp/rotator-latest/scripts/upgrade.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}" +ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}" +TARGET_VERSION_DIR="0.1.3-rotator" + +if [[ "${ENV_FILE}" != /* ]]; then + ENV_FILE="${APP_ROOT}/${ENV_FILE#./}" +fi + +resource_dir_for_app() { + local panel_root app_path app_key + + case "${APP_ROOT}" in + */apps/local/*) + panel_root="${APP_ROOT%%/apps/local/*}" + app_path="${APP_ROOT#"${panel_root}/apps/local/"}" + app_key="${app_path%%/*}" + [[ -n "${panel_root}" && -n "${app_key}" ]] || return 1 + printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}" + ;; + *) + return 1 + ;; + esac +} + +find_rotate_source() { + local resource_dir candidate_upgrade candidate_rotate + + if [[ -n "${ROTATE_SOURCE:-}" ]]; then + printf '%s\n' "${ROTATE_SOURCE}" + return 0 + fi + + if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then + candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh" + candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh" + if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \ + && cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then + printf '%s\n' "${candidate_rotate}" + return 0 + fi + fi + + if [[ -f "${APP_ROOT}/rotate.sh" ]] \ + && grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then + printf '%s\n' "${APP_ROOT}/rotate.sh" + return 0 + fi + + return 1 +} + +strip_matching_quotes() { + local value="$1" + + case "${value}" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + printf '%s\n' "${value}" +} + +rotate_target="${APP_ROOT}/rotate.sh" +rotate_source="$(find_rotate_source || true)" +if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then + echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2 + exit 1 +fi + +if [[ "${rotate_source}" != "${rotate_target}" ]] \ + && ! cmp -s "${rotate_source}" "${rotate_target}"; then + rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")" + # shellcheck disable=SC2317 + cleanup_rotate_tmp() { + rm -f -- "${rotate_tmp}" + } + trap cleanup_rotate_tmp EXIT + + cp -- "${rotate_source}" "${rotate_tmp}" + if [[ -e "${rotate_target}" ]]; then + chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true + else + chmod 0644 "${rotate_tmp}" + fi + mv -f -- "${rotate_tmp}" "${rotate_target}" + trap - EXIT + echo "Updated ${rotate_target} from the target version payload" +fi + +app_data_dir="${APP_DATA_DIR_1:-}" +if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then + app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)" +fi +app_data_dir="$(strip_matching_quotes "${app_data_dir}")" +app_data_dir="${app_data_dir:-./data}" +if [[ "${app_data_dir}" != /* ]]; then + app_data_dir="${APP_ROOT}/${app_data_dir#./}" +fi + +mkdir -p "${app_data_dir}" +exit 0